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

Merged
merged 20 commits into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9313e66
feat(select): Convert JS to TypeScript
acdvorak Feb 9, 2019
135e09d
WIP: Add comment
acdvorak Feb 9, 2019
bc8bf0d
WIP: Fix compiler errors
acdvorak Feb 9, 2019
fb0b996
WIP: Make Istanbul happy
acdvorak Feb 9, 2019
7a7d960
Merge branch 'feat/typescript--select' of github.com:material-compone…
acdvorak Feb 9, 2019
91f5975
WIP: Change `MDCList.listElements` from `HTMLElement[]` back to `Elem…
acdvorak Feb 11, 2019
b50a47a
WIP: `extends CustomEvent<MenuItemEventDetail>`
acdvorak Feb 11, 2019
01edf0f
WIP: Remove Closure annotations
acdvorak Feb 11, 2019
a286663
WIP: Initialize `savedTabIndex_` inline
acdvorak Feb 11, 2019
5facf07
WIP: Remove unnecessary `@material/textfield` dependency from `packag…
acdvorak Feb 11, 2019
7b625f6
Merge remote-tracking branch 'origin/feat/typescript' into feat/types…
acdvorak Feb 11, 2019
f6de964
Merge remote-tracking branch 'origin/feat/typescript' into feat/types…
acdvorak Feb 11, 2019
4fa0e79
WIP: Update filename to `.ts` in `js-bundle-factory.js`
acdvorak Feb 11, 2019
dc34eb6
Merge remote-tracking branch 'origin/feat/typescript' into feat/types…
acdvorak Feb 12, 2019
de38dd6
WIP: Revert argument rename in `MDCComponent.emit()`
acdvorak Feb 12, 2019
01b22be
WIP: Address review comments
acdvorak Feb 12, 2019
ebbbbb9
WIP: Re-indent
acdvorak Feb 12, 2019
a236050
WIP: Address review comments
acdvorak Feb 12, 2019
1e8b7e2
WIP: Throw an error if required subelements are missing
acdvorak Feb 12, 2019
a0cce8c
WIP: Fix `TouchEvent` `clientX` bug
acdvorak Feb 12, 2019
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
9 changes: 3 additions & 6 deletions packages/mdc-base/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,13 @@ class MDCComponent<FoundationType extends MDCFoundation> {
* Fires a cross-browser-compatible custom event from the component root of the given type,
* with the given data.
*/
emit<T extends object>(evtType: string, evtData: T, shouldBubble = false) {
emit<T extends object>(evtType: string, detail: T, bubbles = false) {
acdvorak marked this conversation as resolved.
Show resolved Hide resolved
let evt: CustomEvent<T>;
if (typeof CustomEvent === 'function') {
evt = new CustomEvent<T>(evtType, {
bubbles: shouldBubble,
detail: evtData,
});
evt = new CustomEvent<T>(evtType, {bubbles, detail});
} else {
evt = document.createEvent('CustomEvent');
evt.initCustomEvent(evtType, shouldBubble, false, evtData);
evt.initCustomEvent(evtType, bubbles, false, detail);
}

this.root_.dispatchEvent(evt);
Expand Down
15 changes: 8 additions & 7 deletions packages/mdc-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class MDCList extends MDCComponent<MDCListFoundation> {
this.foundation_.setVerticalOrientation(value);
}

get listElements(): Element[] {
return [].slice.call(this.root_.querySelectorAll(strings.ENABLED_ITEMS_SELECTOR));
get listElements(): HTMLElement[] {
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 guaranteed that this will always be HTMLElement? I think so, but just wondering what you were thinking.

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 think it was because we're calling .focus() on these items in a few places, and I assumed .mdc-list-item would always be an HTMLElement anyway, so I thought we could avoid type assertions by just declaring it an HTMLElement.

However, it looks like it's pretty easy to change this back to Element if you prefer (take a look at commit 91f5975).

I figured it's probably OK to return a more specific subtype, as long as our arguments are still Element for Closure users.

WDYT?

return [].slice.call(this.root_.querySelectorAll<HTMLElement>(strings.ENABLED_ITEMS_SELECTOR));
}

set wrapFocus(value: boolean) {
Expand Down Expand Up @@ -104,16 +104,16 @@ class MDCList extends MDCComponent<MDCListFoundation> {
*/
initializeListType() {
const checkboxListItems = this.root_.querySelectorAll(strings.ARIA_ROLE_CHECKBOX_SELECTOR);
const singleSelectedListItem = this.root_.querySelector(`
const singleSelectedListItem = this.root_.querySelector<HTMLElement>(`
.${cssClasses.LIST_ITEM_ACTIVATED_CLASS},
.${cssClasses.LIST_ITEM_SELECTED_CLASS}
`);
const radioSelectedListItem = this.root_.querySelector(strings.ARIA_CHECKED_RADIO_SELECTOR);
const radioSelectedListItem = this.root_.querySelector<HTMLElement>(strings.ARIA_CHECKED_RADIO_SELECTOR);

if (checkboxListItems.length) {
const preselectedItems = this.root_.querySelectorAll(strings.ARIA_CHECKED_CHECKBOX_SELECTOR);
this.selectedIndex =
[].map.call(preselectedItems, (listItem: Element) => this.listElements.indexOf(listItem)) as number[];
[].map.call(preselectedItems, (listItem: HTMLElement) => this.listElements.indexOf(listItem)) as number[];
} else if (singleSelectedListItem) {
if (singleSelectedListItem.classList.contains(cssClasses.LIST_ITEM_ACTIVATED_CLASS)) {
this.foundation_.setUseActivatedClass(true);
Expand All @@ -140,7 +140,7 @@ class MDCList extends MDCComponent<MDCListFoundation> {
element.focus();
}
},
getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement!),
getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement as HTMLElement),
getListItemCount: () => this.listElements.length,
hasCheckboxAtIndex: (index) => {
const listItem = this.listElements[index];
Expand Down Expand Up @@ -203,7 +203,8 @@ class MDCList extends MDCComponent<MDCListFoundation> {
*/
private getListItemIndex_(evt: Event) {
const eventTarget = evt.target as Element;
const nearestParent = ponyfill.closest(eventTarget, `.${cssClasses.LIST_ITEM_CLASS}, .${cssClasses.ROOT}`);
const nearestParent =
ponyfill.closest<HTMLElement>(eventTarget, `.${cssClasses.LIST_ITEM_CLASS}, .${cssClasses.ROOT}`);

// Get the index of the element if it is a list item.
if (nearestParent && ponyfill.matches(nearestParent, `.${cssClasses.LIST_ITEM_CLASS}`)) {
Expand Down
6 changes: 3 additions & 3 deletions packages/mdc-menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ class MDCMenu extends MDCComponent<MDCMenuFoundation> {
* the items container that are proper list items, and not supplemental / presentational DOM
* elements.
*/
get items(): Element[] {
get items(): HTMLElement[] {
return this.list_ ? this.list_.listElements : [];
}

Expand Down Expand Up @@ -191,10 +191,10 @@ class MDCMenu extends MDCComponent<MDCMenuFoundation> {
},
elementContainsClass: (element, className) => element.classList.contains(className),
closeSurface: () => this.open = false,
getElementIndex: (element) => this.items.indexOf(element),
getElementIndex: (element) => this.items.indexOf(element as HTMLElement),
getParentElement: (element) => element.parentElement,
getSelectedElementIndex: (selectionGroup) => {
const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
const selectedListItem = selectionGroup.querySelector<HTMLElement>(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`);
return selectedListItem ? this.items.indexOf(selectedListItem) : -1;
},
notifySelected: (evtData) => this.emit<DefaultMenuItemEventDetail>(strings.SELECTED_EVENT, {
Expand Down
8 changes: 8 additions & 0 deletions packages/mdc-menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
import {MDCList} from '@material/list/index';
import {MDCMenuSurface} from '@material/menu-surface/index';

export interface MenuItemEvent extends Event {
acdvorak marked this conversation as resolved.
Show resolved Hide resolved
detail: MenuItemEventDetail;
}

export interface DefaultMenuItemEvent extends Event {
acdvorak marked this conversation as resolved.
Show resolved Hide resolved
detail: DefaultMenuItemEventDetail;
}

/**
* Event properties used by the adapter and foundation.
*/
Expand Down
18 changes: 9 additions & 9 deletions packages/mdc-select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,14 +407,14 @@ The `MDCSelect` component API is modeled after a subset of the `HTMLSelectElemen

| Property | Type | Description |
| --- | --- | --- |
| `value` | string | The `value`/`data-value` of the currently selected option. |
| `selectedIndex` | number | The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element. |
| `disabled` | boolean | Whether or not the component is disabled. Setting this sets the disabled state on the component. |
| `valid` | boolean | Whether or not the component is in a valid state. Setting this updates styles on the component, but does not affect the native validity state. |
| `required` | boolean | Whether or not the component is required. Setting this updates the `required` or `aria-required` attribute on the component and enables validation. |
| `leadingIconAriaLabel` | string (write-only) | Proxies to the foundation's `setLeadingIconAriaLabel` method. |
| `leadingIconContent` | string (write-only) | Proxies to the foundation's `setLeadingIconContent` method. |
| `helperTextContent` | string (write-only)| Proxies to the foundation's `setHelperTextContent` method when set. |
| `value` | `string` | The `value`/`data-value` of the currently selected option. |
| `selectedIndex` | `number` | The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element. |
| `disabled` | `boolean` | Whether or not the component is disabled. Setting this sets the disabled state on the component. |
| `valid` | `boolean` | Whether or not the component is in a valid state. Setting this updates styles on the component, but does not affect the native validity state. |
| `required` | `boolean` | Whether or not the component is required. Setting this updates the `required` or `aria-required` attribute on the component and enables validation. |
| `leadingIconAriaLabel` | `string` (write-only) | Proxies to the foundation's `setLeadingIconAriaLabel` method. |
| `leadingIconContent` | `string` (write-only) | Proxies to the foundation's `setLeadingIconContent` method. |
| `helperTextContent` | `string` (write-only)| Proxies to the foundation's `setHelperTextContent` method when set. |

### Events

Expand Down Expand Up @@ -462,7 +462,7 @@ If you are using a JavaScript framework, such as React or Angular, you can creat
| `handleBlur() => void` | Handles a blur event on the `select` element. |
| `handleClick(normalizedX: number) => void` | Sets the line ripple center to the normalizedX for the line ripple. |
| `handleChange() => void` | Handles a change to the `select` element's value. This must be called both for `change` events and programmatic changes requested via the component API. |
| `handleKeydown(event: Event) => void` | Handles opening the menu (enhanced select) when the `mdc-select__selected-text` element is focused and the user presses the `Enter` or `Space` key. |
| `handleKeydown(event: KeyboardEvent) => void` | Handles opening the menu (enhanced select) when the `mdc-select__selected-text` element is focused and the user presses the `Enter` or `Space` key. |
| `setSelectedIndex(index: number) => void` | Handles setting the `mdc-select__selected-text` element and closing the menu (enhanced select only). Also causes the label to float and outline to notch if needed. |
| `setValue(value: string) => void` | Handles setting the value through the adapter and causes the label to float and outline to notch if needed. |
| `getValue() => string` | Handles getting the value through the adapter. |
Expand Down
89 changes: 24 additions & 65 deletions packages/mdc-select/adapter.js → packages/mdc-select/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,159 +21,118 @@
* THE SOFTWARE.
*/

/* eslint no-unused-vars: [2, {"args": "none"}] */
/* eslint-disable no-unused-vars */
import {MDCSelectIconFoundation} from './icon/index';
import {MDCSelectHelperTextFoundation} from './helper-text/index';
/* eslint-enable no-unused-vars */

/**
* @typedef {{
* leadingIcon: (!MDCSelectIconFoundation|undefined),
* helperText: (!MDCSelectHelperTextFoundation|undefined),
* }}
*/
let FoundationMapType;

/**
* Adapter for MDC Select. Provides an interface for managing
* - classes
* - dom
* - event handlers
*
* Additionally, provides type information for the adapter to the Closure
* compiler.
*
* 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
*
* @record
*/

class MDCSelectAdapter {
interface MDCSelectAdapter {
/**
* Adds class to 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 name.
* @param {string} className
* @return {boolean}
*/
hasClass(className) {}
hasClass(className: string): boolean;

/**
* Activates the bottom line, showing a focused state.
*/
activateBottomLine() {}
activateBottomLine(): void;

/**
* Deactivates the bottom line.
*/
deactivateBottomLine() {}
deactivateBottomLine(): void;

/**
* Sets the value of the select.
* @param {string} value
*/
setValue(value) {}
setValue(value: string): void;

/**
* Returns the selected value of the select element.
* @return {string}
*/
getValue() {}
getValue(): string;

/**
* Floats label determined based off of the shouldFloat argument.
* @param {boolean} shouldFloat
*/
floatLabel(shouldFloat) {}
floatLabel(shouldFloat: boolean): void;

/**
* Returns width of label in pixels, if the label exists.
* @return {number}
*/
getLabelWidth() {}
getLabelWidth(): number;

/**
* Returns true if outline element exists, false if it doesn't.
* @return {boolean}
*/
hasOutline() {}
hasOutline(): boolean;

/**
* Only implement if outline element exists.
* @param {number} labelWidth
*/
notchOutline(labelWidth) {}
notchOutline(labelWidth: number): void;

/**
* Closes notch in outline element, if the outline exists.
*/
closeOutline() {}
closeOutline(): void;

/**
* Opens the menu.
*/
openMenu() {}
openMenu(): void;

/**
* Closes the menu.
*/
closeMenu() {}
closeMenu(): void;

/**
* Returns true if the menu is currently open.
* @return {boolean}
*/
isMenuOpen() {}
isMenuOpen(): boolean;

/**
* Sets the selected index of the select to the index provided.
* @param {number} index
*/
setSelectedIndex(index) {}
setSelectedIndex(index: number): void;

/**
* Sets the select to disabled.
* @param {boolean} isDisabled
*/
setDisabled(isDisabled) {}
setDisabled(isDisabled: boolean): void;

/**
* Sets the line ripple transform origin center.
* @param {number} normalizedX
*/
setRippleCenter(normalizedX) {}
setRippleCenter(normalizedX: number): void;

/**
* Emits a change event when an element is selected.
* @param {string} value
*/
notifyChange(value) {}
notifyChange(value: string): void;

/**
* Checks if the select is currently valid.
* @return {boolean} isValid
*/
checkValidity() {}
checkValidity(): boolean;

/**
* Adds/Removes the invalid class.
* @param {boolean} isValid
*/
setValid(isValid) {}
setValid(isValid: boolean): void;
}

export {MDCSelectAdapter, FoundationMapType};
export {MDCSelectAdapter as default, MDCSelectAdapter};
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,33 @@
* THE SOFTWARE.
*/

/** @enum {string} */
const cssClasses = {
DISABLED: 'mdc-select--disabled',
ROOT: 'mdc-select',
OUTLINED: 'mdc-select--outlined',
FOCUSED: 'mdc-select--focused',
SELECTED_ITEM_CLASS: 'mdc-list-item--selected',
WITH_LEADING_ICON: 'mdc-select--with-leading-icon',
INVALID: 'mdc-select--invalid',
OUTLINED: 'mdc-select--outlined',
REQUIRED: 'mdc-select--required',
ROOT: 'mdc-select',
SELECTED_ITEM_CLASS: 'mdc-list-item--selected',
WITH_LEADING_ICON: 'mdc-select--with-leading-icon',
};

/** @enum {string} */
const strings = {
ARIA_CONTROLS: 'aria-controls',
ARIA_SELECTED_ATTR: 'aria-selected',
CHANGE_EVENT: 'MDCSelect:change',
SELECTED_ITEM_SELECTOR: `.${cssClasses.SELECTED_ITEM_CLASS}`,
LEADING_ICON_SELECTOR: '.mdc-select__icon',
SELECTED_TEXT_SELECTOR: '.mdc-select__selected-text',
ENHANCED_VALUE_ATTR: 'data-value',
HIDDEN_INPUT_SELECTOR: 'input[type="hidden"]',
MENU_SELECTOR: '.mdc-select__menu',
LINE_RIPPLE_SELECTOR: '.mdc-line-ripple',
LABEL_SELECTOR: '.mdc-floating-label',
LEADING_ICON_SELECTOR: '.mdc-select__icon',
LINE_RIPPLE_SELECTOR: '.mdc-line-ripple',
MENU_SELECTOR: '.mdc-select__menu',
NATIVE_CONTROL_SELECTOR: '.mdc-select__native-control',
OUTLINE_SELECTOR: '.mdc-notched-outline',
ENHANCED_VALUE_ATTR: 'data-value',
ARIA_SELECTED_ATTR: 'aria-selected',
SELECTED_ITEM_SELECTOR: `.${cssClasses.SELECTED_ITEM_CLASS}`,
SELECTED_TEXT_SELECTOR: '.mdc-select__selected-text',
};

/** @enum {number} */
const numbers = {
LABEL_SCALE: 0.75,
};
Expand Down
Loading