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(drawer): Convert JS to TypeScript #4390

Merged
merged 15 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/mdc-dialog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class MDCDialog extends MDCComponent<MDCDialogFoundation> {
private container_!: HTMLElement; // assigned in initialize()
private content_!: HTMLElement | null; // assigned in initialize()
private defaultButton_!: HTMLElement | null; // assigned in initialize()
private initialFocusEl_!: HTMLElement | null; // assigned in initialize()
private initialFocusEl_!: HTMLElement | undefined; // assigned in initialize()
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a contradiction in terms. Isn't this effectively private initialFocusEl_?: HTMLElement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Confusingly, the ! here has a slightly different meaning than when it's used after an expression.

When declaring member properties, ! is a "definite assignment assertion", meaning "I pinky swear that some value is assigned to this property, but it happens somewhere other than the constructor.

In an expression (e.g., querySelector('.foo')!), the bang means "this value will never be null or undefined."

You make a good point though; this can be shortened to private initialFocusEl_?: HTMLElement.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


private focusTrap_!: createFocusTrap.FocusTrap; // assigned in initialSyncWithDOM()
private focusTrapFactory_!: FocusTrapFactory; // assigned in initialize()
Expand All @@ -83,7 +83,7 @@ class MDCDialog extends MDCComponent<MDCDialogFoundation> {

initialize(
focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory,
initialFocusEl: Element | null = null) {
initialFocusEl?: Element) {
const container = this.root_.querySelector<HTMLElement>(strings.CONTAINER_SELECTOR);
if (!container) {
throw new Error(`Dialog component requires a ${strings.CONTAINER_SELECTOR} container element`);
Expand All @@ -93,7 +93,7 @@ class MDCDialog extends MDCComponent<MDCDialogFoundation> {
this.buttons_ = [].slice.call(this.root_.querySelectorAll<HTMLElement>(strings.BUTTON_SELECTOR));
this.defaultButton_ = this.root_.querySelector<HTMLElement>(strings.DEFAULT_BUTTON_SELECTOR);
this.focusTrapFactory_ = focusTrapFactory;
this.initialFocusEl_ = initialFocusEl as HTMLElement;
this.initialFocusEl_ = initialFocusEl as HTMLElement | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we cast to HTMLElement here when we're accepting Element above? Is there something we need from HTMLElement in particular?

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 was trying to make things simpler for Closure users (who typically pass an Element).

However, this method is called indirectly via the MDCComponent constructor (which takes an Element), so it actually shouldn't be an issue for them.

I'll change the type of the initialFocusEl arg to HTMLElement. The TypeScript compiler seems fine with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.buttonRipples_ = [];

for (const buttonEl of this.buttons_) {
Expand Down
18 changes: 8 additions & 10 deletions packages/mdc-dialog/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,24 @@
import * as createFocusTrap from 'focus-trap';
import {FocusTrapFactory} from './types';

function createFocusTrapInstance(
export function createFocusTrapInstance(
surfaceEl: HTMLElement,
focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory,
initialFocusEl: createFocusTrap.FocusTarget | null,
initialFocusEl?: createFocusTrap.FocusTarget,
): createFocusTrap.FocusTrap {
return focusTrapFactory(surfaceEl, ({
clickOutsideDeactivates: true, // Allow handling of scrim clicks
escapeDeactivates: false, // Dialog foundation handles escape key
return focusTrapFactory(surfaceEl, {
clickOutsideDeactivates: true, // Allow handling of scrim clicks.
escapeDeactivates: false, // Foundation handles ESC key.
initialFocus: initialFocusEl,
} as createFocusTrap.Options));
});
}

function isScrollable(el: HTMLElement | null): boolean {
export function isScrollable(el: HTMLElement | null): boolean {
return el ? el.scrollHeight > el.offsetHeight : false;
}

function areTopsMisaligned(els: HTMLElement[]): boolean {
export function areTopsMisaligned(els: HTMLElement[]): boolean {
const tops = new Set();
[].forEach.call(els, (el: HTMLElement) => tops.add(el.offsetTop));
return tops.size > 1;
}

export {createFocusTrapInstance, isScrollable, areTopsMisaligned};
49 changes: 20 additions & 29 deletions packages/mdc-drawer/adapter.js → packages/mdc-drawer/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,79 +21,70 @@
* THE SOFTWARE.
*/

/* eslint no-unused-vars: [2, {"args": "none"}] */

/**
* Adapter for MDC Drawer
*
* Defines the shape of the adapter expected by the foundation. Implement this
* adapter to integrate the Drawer into your framework. See
* https://github.com/material-components/material-components-web/blob/master/docs/authoring-components.md
* for more information.
*
* @record
* Defines the shape of the adapter expected by the foundation.
* Implement this adapter for your framework of choice to delegate updates to
* the component in your framework of choice. See architecture documentation
* for more details.
* https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md
*/
class MDCDrawerAdapter {
interface MDCDrawerAdapter {
/**
* Adds a class to the root Element.
* @param {string} className
*/
addClass(className) {}
addClass(className: string): void;

/**
* Removes a class from the root Element.
* @param {string} className
*/
removeClass(className) {}
removeClass(className: string): void;

/**
* Returns true if the root Element contains the given class.
* @param {string} className
* @return {boolean}
*/
hasClass(className) {}
hasClass(className: string): boolean;

/**
* @param {!Element} element target element to verify class name
* @param {string} className class name
* @param element target element to verify class name
* @param className class name
*/
elementHasClass(element, className) {}
elementHasClass(element: Element, className: string): boolean;

/**
* Saves the focus of currently active element.
*/
saveFocus() {}
saveFocus(): void;

/**
* Restores focus to element previously saved with 'saveFocus'.
*/
restoreFocus() {}
restoreFocus(): void;

/**
* Focuses the active / selected navigation item.
*/
focusActiveNavigationItem() {}
focusActiveNavigationItem(): void;

/**
* Emits a custom event "MDCDrawer:closed" denoting the drawer has closed.
*/
notifyClose() {}
notifyClose(): void;

/**
* Emits a custom event "MDCDrawer:opened" denoting the drawer has opened.
*/
notifyOpen() {}
notifyOpen(): void;

/**
* Traps focus on root element and focuses the active navigation element.
*/
trapFocus() {}
trapFocus(): void;

/**
* Releases focus trap from root element which was set by `trapFocus`
* and restores focus to where it was prior to calling `trapFocus`.
*/
releaseFocus() {}
releaseFocus(): void;
}

export default MDCDrawerAdapter;
export {MDCDrawerAdapter as default, MDCDrawerAdapter};
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/** @enum {string} */

const cssClasses = {
ROOT: 'mdc-drawer',
ANIMATE: 'mdc-drawer--animate',
CLOSING: 'mdc-drawer--closing',
DISMISSIBLE: 'mdc-drawer--dismissible',
MODAL: 'mdc-drawer--modal',
OPEN: 'mdc-drawer--open',
ANIMATE: 'mdc-drawer--animate',
OPENING: 'mdc-drawer--opening',
CLOSING: 'mdc-drawer--closing',
ROOT: 'mdc-drawer',
};

/** @enum {string} */
const strings = {
APP_CONTENT_SELECTOR: '.mdc-drawer-app-content',
SCRIM_SELECTOR: '.mdc-drawer-scrim',
CLOSE_EVENT: 'MDCDrawer:closed',
OPEN_EVENT: 'MDCDrawer:opened',
SCRIM_SELECTOR: '.mdc-drawer-scrim',
};

export {cssClasses, strings};
Original file line number Diff line number Diff line change
Expand Up @@ -21,48 +21,42 @@
* THE SOFTWARE.
*/

import MDCDrawerAdapter from '../adapter';
import {MDCFoundation} from '@material/base/foundation';
import {MDCDrawerAdapter} from '../adapter';
import {cssClasses, strings} from '../constants';

/**
* @extends {MDCFoundation<!MDCDrawerAdapter>}
*/
class MDCDismissibleDrawerFoundation extends MDCFoundation {
/** @return enum {string} */
class MDCDismissibleDrawerFoundation extends MDCFoundation<MDCDrawerAdapter> {
static get strings() {
return strings;
}

/** @return enum {string} */
static get cssClasses() {
return cssClasses;
}

static get defaultAdapter() {
return /** @type {!MDCDrawerAdapter} */ ({
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
hasClass: (/* className: string */) => {},
elementHasClass: (/* element: !Element, className: string */) => {},
notifyClose: () => {},
notifyOpen: () => {},
saveFocus: () => {},
restoreFocus: () => {},
focusActiveNavigationItem: () => {},
trapFocus: () => {},
releaseFocus: () => {},
});
static get defaultAdapter(): MDCDrawerAdapter {
// tslint:disable:object-literal-sort-keys
return {
addClass: () => undefined,
removeClass: () => undefined,
hasClass: () => false,
elementHasClass: () => false,
notifyClose: () => undefined,
notifyOpen: () => undefined,
saveFocus: () => undefined,
restoreFocus: () => undefined,
focusActiveNavigationItem: () => undefined,
trapFocus: () => undefined,
releaseFocus: () => undefined,
};
// tslint:enable:object-literal-sort-keys
}

constructor(adapter) {
super(Object.assign(MDCDismissibleDrawerFoundation.defaultAdapter, adapter));

/** @private {number} */
this.animationFrame_ = 0;
private animationFrame_ = 0;
private animationTimer_ = 0;

/** @private {number} */
this.animationTimer_ = 0;
constructor(adapter?: Partial<MDCDrawerAdapter>) {
super({...MDCDismissibleDrawerFoundation.defaultAdapter, ...adapter});
}

destroy() {
Expand All @@ -74,9 +68,6 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation {
}
}

/**
* Function to open the drawer.
*/
open() {
if (this.isOpen() || this.isOpening() || this.isClosing()) {
return;
Expand All @@ -93,9 +84,6 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation {
this.adapter_.saveFocus();
}

/**
* Function to close the drawer.
*/
close() {
if (!this.isOpen() || this.isOpening() || this.isClosing()) {
return;
Expand All @@ -105,48 +93,31 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation {
}

/**
* Extension point for when drawer finishes open animation.
* @protected
* @return true if drawer is in open state.
*/
opened() {}

/**
* Extension point for when drawer finishes close animation.
* @protected
*/
closed() {}

/**
* Returns true if drawer is in open state.
* @return {boolean}
*/
isOpen() {
isOpen(): boolean {
return this.adapter_.hasClass(cssClasses.OPEN);
}

/**
* Returns true if drawer is animating open.
* @return {boolean}
* @return true if drawer is animating open.
*/
isOpening() {
isOpening(): boolean {
return this.adapter_.hasClass(cssClasses.OPENING) || this.adapter_.hasClass(cssClasses.ANIMATE);
}

/**
* Returns true if drawer is animating closed.
* @return {boolean}
* @return true if drawer is animating closed.
*/
isClosing() {
isClosing(): boolean {
return this.adapter_.hasClass(cssClasses.CLOSING);
}

/**
* Keydown handler to close drawer when key is escape.
* @param evt
*/
handleKeydown(evt) {
handleKeydown(evt: KeyboardEvent) {
const {keyCode, key} = evt;

const isEscape = key === 'Escape' || keyCode === 27;
if (isEscape) {
this.close();
Expand All @@ -155,14 +126,13 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation {

/**
* Handles a transition end event on the root element.
* @param {!Event} evt
*/
handleTransitionEnd(evt) {
handleTransitionEnd(evt: TransitionEvent) {
const {OPENING, CLOSING, OPEN, ANIMATE, ROOT} = cssClasses;

// In Edge, transitionend on ripple pseudo-elements yields a target without classList, so check for Element first.
const isElement = evt.target instanceof Element;
if (!isElement || !this.adapter_.elementHasClass(/** @type {!Element} */ (evt.target), ROOT)) {
const isRootElement = this.isElement_(evt.target) && this.adapter_.elementHasClass(evt.target, ROOT);
if (!isRootElement) {
return;
}

Expand All @@ -182,19 +152,32 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation {
this.adapter_.removeClass(CLOSING);
}

/**
* Extension point for when drawer finishes open animation.
*/
protected opened() {} // tslint:disable-line:no-empty

/**
* Extension point for when drawer finishes close animation.
*/
protected closed() {} // tslint:disable-line:no-empty

/**
* Runs the given logic on the next animation frame, using setTimeout to factor in Firefox reflow behavior.
* @param {Function} callback
* @private
*/
runNextAnimationFrame_(callback) {
private runNextAnimationFrame_(callback: () => void) {
cancelAnimationFrame(this.animationFrame_);
this.animationFrame_ = requestAnimationFrame(() => {
this.animationFrame_ = 0;
clearTimeout(this.animationTimer_);
this.animationTimer_ = setTimeout(callback, 0);
});
}

private isElement_(element: unknown): element is Element {
// In Edge, transitionend on ripple pseudo-elements yields a target without classList.
return Boolean((element as Element).classList);
}
}

export default MDCDismissibleDrawerFoundation;
export {MDCDismissibleDrawerFoundation as default, MDCDismissibleDrawerFoundation};
Loading