Skip to content

Commit

Permalink
feat(select): Move focus handling to surface element for focus shade (#…
Browse files Browse the repository at this point in the history
…1803)

BREAKING CHANGE: JS-enhanced Select should now apply tabindex to the surface element instead of the root element. The adapter APIs related to focus, interaction handling, and tabbability now operate on the surface element instead of the root element.
  • Loading branch information
kfranqueiro authored Dec 20, 2017
1 parent ee1c0db commit 255b63e
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 64 deletions.
8 changes: 4 additions & 4 deletions demos/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
<main>
<div class="mdc-toolbar-fixed-adjust"></div>
<section class="hero">
<div id="hero-js-select" class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div id="hero-js-select" class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -99,8 +99,8 @@
<section class="example">
<h2 class="mdc-typography--title">Fully-Featured Component</h2>
<section id="box-demo-wrapper">
<div id="js-select-box" class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div id="js-select-box" class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down
4 changes: 2 additions & 2 deletions demos/theme/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,8 @@ <h2 class="mdc-typography--headline demo-component-section__heading">
<a href="#select" class="demo-component-section__permalink" title="Permalink to the theme demo for the select component">#</a>
</h2>

<div class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a food group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down
32 changes: 16 additions & 16 deletions packages/mdc-select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ npm install --save @material/select
### Using the full-fidelity JS component

```html
<div class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -98,8 +98,8 @@ style dependencies for both the mdc-list and mdc-menu for this component to func
#### Select with pre-selected option

```html
<div class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -132,8 +132,8 @@ style dependencies for both the mdc-list and mdc-menu for this component to func
#### Disabled select

```html
<div class="mdc-select" role="listbox" aria-disabled="true" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox" aria-disabled="true">
<div class="mdc-select__surface" tabindex="-1">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -165,12 +165,12 @@ style dependencies for both the mdc-list and mdc-menu for this component to func

#### Disabled options

When used in components such as MDC Select, `mdc-list-item`'s can be disabled.
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" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -358,11 +358,11 @@ within `componentDidUpdate`.
| `setAttr(attr: string, value: string) => void` | Sets attribute `attr` to value `value` on the root element. |
| `rmAttr(attr: string) => void` | Removes attribute `attr` from the root element. |
| `computeBoundingRect() => {left: number, top: number}` | Returns an object with a shape similar to a `ClientRect` object, with a `left` and `top` property specifying the element's position on the page relative to the viewport. The easiest way to achieve this is by calling `getBoundingClientRect()` on the surface element. |
| `registerInteractionHandler(type: string, handler: EventListener) => void` | Adds an event listener `handler` for event type `type` on the root element. |
| `deregisterInteractionHandler(type: string, handler: EventListener) => void` | Removes an event listener `handler` for event type `type` on the root element. |
| `focus() => void` | Focuses the root element |
| `makeTabbable() => void` | Allows the root element to be tab-focused via keyboard. We achieve this by setting the root element's `tabIndex` property to `0`. |
| `makeUntabbable() => void` | Disallows the root element to be tab-focused via keyboard. We achieve this by setting the root element's `tabIndex` property to `-1`. |
| `registerInteractionHandler(type: string, handler: EventListener) => void` | Adds an event listener `handler` for event type `type` on the surface element. |
| `deregisterInteractionHandler(type: string, handler: EventListener) => void` | Removes an event listener `handler` for event type `type` on the surface element. |
| `focus() => void` | Focuses the surface element |
| `makeTabbable() => void` | Allows the surface element to be tab-focused via keyboard. We achieve this by setting the surface element's `tabIndex` property to `0`. |
| `makeUntabbable() => void` | Disallows the surface element from being tab-focused via keyboard. We achieve this by setting the surface element's `tabIndex` property to `-1`. |
| `getComputedStyleValue(propertyName: string) => string` | Get the surface element's computed style value of the given dasherized css property `propertyName`. We achieve this via `getComputedStyle(...).getPropertyValue(propertyName). `|
| `setStyle(propertyName: string, value: string) => void` | Sets a dasherized css property `propertyName` to the value `value` on the surface element. We achieve this via `root.style.setProperty(propertyName, value)`. |
| `create2dRenderingContext() => {font: string, measureText: (string) => {width: number}}` | Returns an object which has the shape of a CanvasRenderingContext2d instance. Namely, it has a string property `font` which is writable, and a method `measureText` which given a string of text returns an object containing a `width` property specifying how wide that text should be rendered in the `font` specified by the font property. An easy way to achieve this is simply `document.createElement('canvas').getContext('2d');`. |
Expand Down Expand Up @@ -426,8 +426,8 @@ First, wrap both a custom select and a native select within a wrapper element, l
```html
<div class="select-manager">
<!-- Custom MDC Select, shown on desktop -->
<div class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick One</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down
10 changes: 5 additions & 5 deletions packages/mdc-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ export class MDCSelect extends MDCComponent {
setAttr: (attr, value) => this.root_.setAttribute(attr, value),
rmAttr: (attr, value) => this.root_.removeAttribute(attr, value),
computeBoundingRect: () => this.surface_.getBoundingClientRect(),
registerInteractionHandler: (type, handler) => this.root_.addEventListener(type, handler),
deregisterInteractionHandler: (type, handler) => this.root_.removeEventListener(type, handler),
focus: () => this.root_.focus(),
registerInteractionHandler: (type, handler) => this.surface_.addEventListener(type, handler),
deregisterInteractionHandler: (type, handler) => this.surface_.removeEventListener(type, handler),
focus: () => this.surface_.focus(),
makeTabbable: () => {
this.root_.tabIndex = 0;
this.surface_.tabIndex = 0;
},
makeUntabbable: () => {
this.root_.tabIndex = -1;
this.surface_.tabIndex = -1;
},
getComputedStyleValue: (prop) => window.getComputedStyle(this.surface_).getPropertyValue(prop),
setStyle: (propertyName, value) => this.surface_.style.setProperty(propertyName, value),
Expand Down
29 changes: 14 additions & 15 deletions packages/mdc-select/mdc-select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,6 @@ $mdc-select-menu-transition: transform 180ms $mdc-animation-standard-curve-timin
background-position: left 10px center;
}

&:focus {
.mdc-select__bottom-line {
@include mdc-theme-prop(background-color, primary);

transform: scaleY(2);

&::after {
opacity: 1;
}
}
}

@include mdc-theme-dark(".mdc-select") {
@include mdc-select-dd-arrow-svg-bg_("fff", .54);

Expand Down Expand Up @@ -184,17 +172,28 @@ $mdc-select-menu-transition: transform 180ms $mdc-animation-standard-curve-timin
}
}

// Since the CSS only version's .mdc-select__surface element
// is an actual <select> element (and as such gets focus), this
// will only apply to CSS only selects
// JS-enhanced and CSS-only Selects require different selectors for focused bottom-line due to different DOM structure
&__surface:focus .mdc-select__bottom-line,
&__surface:focus ~ .mdc-select__bottom-line {
@include mdc-theme-prop(background-color, primary);

transform: scaleY(2);

&::after {
opacity: 1;
}
}
}

.mdc-select--open {
.mdc-select__surface::before {
opacity: map-get($mdc-ripple-dark-ink-opacities, "focus");

@include mdc-theme-dark(".mdc-select") {
opacity: map-get($mdc-ripple-light-ink-opacities, "focus");
}
}

.mdc-select__selected-text {
transform: translateY(8px);
transition:
Expand Down
42 changes: 20 additions & 22 deletions test/unit/mdc-select/mdc-select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class FakeMenu {

function getFixture() {
return bel`
<div class="mdc-select" role="listbox" tabindex="0">
<div class="mdc-select__surface">
<div class="mdc-select" role="listbox">
<div class="mdc-select__surface" tabindex="0">
<div class="mdc-select__label">Pick a Food Group</div>
<div class="mdc-select__selected-text"></div>
<div class="mdc-select__bottom-line"></div>
Expand Down Expand Up @@ -231,47 +231,45 @@ test('adapter#computeBoundingRect returns the result of getBoundingClientRect()
);
});

test('adapter#registerInteractionHandler adds an event listener to the root element', () => {
const {component, fixture} = setupTest();
test('adapter#registerInteractionHandler adds an event listener to the surface element', () => {
const {component, surface} = setupTest();
const listener = td.func('eventlistener');
component.getDefaultFoundation().adapter_.registerInteractionHandler('click', listener);
domEvents.emit(fixture, 'click');
domEvents.emit(surface, 'click');
td.verify(listener(td.matchers.anything()));
});

test('adapter#deregisterInteractionHandler removes an event listener from the root element', () => {
const {component, fixture} = setupTest();
test('adapter#deregisterInteractionHandler removes an event listener from the surface element', () => {
const {component, surface} = setupTest();
const listener = td.func('eventlistener');
fixture.addEventListener('click', listener);
surface.addEventListener('click', listener);
component.getDefaultFoundation().adapter_.deregisterInteractionHandler('click', listener);
domEvents.emit(fixture, 'click');
domEvents.emit(surface, 'click');
td.verify(listener(td.matchers.anything()), {times: 0});
});

test('adapter#focus focuses on the root element', () => {
const {component, fixture} = setupTest();
const handler = td.func('fixture focus handler');
fixture.addEventListener('focus', handler);
test('adapter#focus focuses on the surface element', () => {
const {component, fixture, surface} = setupTest();
document.body.appendChild(fixture);

component.getDefaultFoundation().adapter_.focus();
assert.equal(document.activeElement, fixture);
assert.equal(document.activeElement, surface);

document.body.removeChild(fixture);
});

test('adapter#makeTabbable sets the root element\'s tabindex to 0', () => {
const {component, fixture} = setupTest();
fixture.tabIndex = -1;
test('adapter#makeTabbable sets the surface element\'s tabindex to 0', () => {
const {component, surface} = setupTest();
surface.tabIndex = -1;
component.getDefaultFoundation().adapter_.makeTabbable();
assert.equal(fixture.tabIndex, 0);
assert.equal(surface.tabIndex, 0);
});

test('adapter#makeUntabbable sets the root element\'s tabindex to -1', () => {
const {component, fixture} = setupTest();
fixture.tabIndex = 0;
test('adapter#makeUntabbable sets the surface element\'s tabindex to -1', () => {
const {component, surface} = setupTest();
surface.tabIndex = 0;
component.getDefaultFoundation().adapter_.makeUntabbable();
assert.equal(fixture.tabIndex, -1);
assert.equal(surface.tabIndex, -1);
});

test('adapter#getComputedStyleValue gets the computed style value of the prop from the surface element', () => {
Expand Down

0 comments on commit 255b63e

Please sign in to comment.