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

Conversation

acdvorak
Copy link
Contributor

@acdvorak acdvorak commented Feb 9, 2019

Refs #4225

packages/mdc-select/types.ts Outdated Show resolved Hide resolved
@codecov-io
Copy link

codecov-io commented Feb 9, 2019

Codecov Report

Merging #4386 into feat/typescript will increase coverage by <.01%.
The diff coverage is 98.79%.

Impacted file tree graph

@@                 Coverage Diff                 @@
##           feat/typescript    #4386      +/-   ##
===================================================
+ Coverage            98.68%   98.69%   +<.01%     
===================================================
  Files                   94       95       +1     
  Lines                 5987     6032      +45     
  Branches               805      797       -8     
===================================================
+ Hits                  5908     5953      +45     
  Misses                  78       78              
  Partials                 1        1
Impacted Files Coverage Δ
packages/mdc-base/component.ts 96.42% <ø> (ø) ⬆️
packages/mdc-select/helper-text/foundation.ts 100% <100%> (ø)
packages/mdc-select/foundation.ts 97.6% <100%> (ø)
packages/mdc-select/icon/index.ts 100% <100%> (ø)
packages/mdc-select/helper-text/index.ts 100% <100%> (ø)
packages/mdc-list/index.ts 98.51% <100%> (ø) ⬆️
packages/mdc-select/icon/foundation.ts 100% <100%> (ø)
packages/mdc-select/index.ts 98.39% <98.39%> (ø)
packages/mdc-base/index.ts 100% <0%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b2fa9a5...a0cce8c. Read the comment docs.

@mdc-web-bot
Copy link
Collaborator

All 621 screenshot tests passed for commit bc8bf0d vs. feat/typescript! 💯🎉

@mdc-web-bot
Copy link
Collaborator

All 621 screenshot tests passed for commit 7a7d960 vs. feat/typescript! 💯🎉

@@ -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?

packages/mdc-menu/types.ts Outdated Show resolved Hide resolved
packages/mdc-menu/types.ts Outdated Show resolved Hide resolved
packages/mdc-select/foundation.ts Outdated Show resolved Hide resolved
packages/mdc-select/icon/foundation.ts Outdated Show resolved Hide resolved
packages/mdc-select/package.json Outdated Show resolved Hide resolved
@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit f6de964 vs. feat/typescript! 💯🎉

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit 4fa0e79 vs. feat/typescript! 💯🎉

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit dc34eb6 vs. feat/typescript! 💯🎉

packages/mdc-base/component.ts Outdated Show resolved Hide resolved
@@ -135,7 +135,7 @@ class MDCList extends MDCComponent<MDCListFoundation> {
}
},
focusItemAtIndex: (index) => {
const element = this.listElements[index] as HTMLElement;
const element = this.listElements[index] as HTMLElement | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this technically be undefined, not null, if it didn't exist? What does this fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are technically correct! 😀 Fixed.

There's a subtle but important difference between a type assertion with and without | null (or | undefined):

// This tells the TypeScript compiler that `element` will
// NEVER be null or undefined, so it won't yell at you
// if you try to access a property on `element`, which is dangerous.
const element = this.listElements[index] as HTMLElement;

// This tells tsc that `element` might not have a value, so it should
// yell at you if you try to access a property without a truthy check.
const element = this.listElements[index] as HTMLElement | undefined;

In this specific case, it doesn't matter quite as much because we already have a truthy check below (if (element) { ... }), but if we ever refactor this code, we could easily forget that element might be undefined.

Personally, I'd rather be semantically accurate by declaring that the type might be undefined.

@@ -24,6 +24,9 @@
import {MDCList} from '@material/list/index';
import {MDCMenuSurface} from '@material/menu-surface/index';

export type MenuItemEvent = CustomEvent<MenuItemEventDetail>;
export type DefaultMenuItemEvent = CustomEvent<DefaultMenuItemEventDetail>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I overlooked this in the original Menu PR but I found the Default here confusing. Unsure whether #4407 could leave room for resolving this.

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 had a hard time figuring out what to call this type, so I'm definitely open to suggestions.

  • MenuItemEvent is only used by foundation and adapter
  • DefaultMenuItemEvent is only used by the component, and has an additional property on its detail object

What would you prefer to call it?

packages/mdc-select/helper-text/index.ts Outdated Show resolved Hide resolved
packages/mdc-select/icon/README.md Outdated Show resolved Hide resolved
packages/mdc-select/index.ts Outdated Show resolved Hide resolved
packages/mdc-select/index.ts Outdated Show resolved Hide resolved
packages/mdc-select/index.ts Outdated Show resolved Hide resolved
packages/mdc-select/index.ts Outdated Show resolved Hide resolved
index: number;
}

// TODO(acdvorak): Every component should export its own factory and event types.
Copy link
Contributor

Choose a reason for hiding this comment

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

Define "every"? This is pretty particular to text field and select sub-components.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A number of components need to instantiate ripples, icons, etc. I figured we might as well just make it a standard pattern to always export a factory type for every component so that we don't end up with duplicate factory definitions in lots of different components.

element.addEventListener('blur', this.handleBlur_);

MOUSEDOWN_TOUCHSTART_EVENTS.forEach((evtType) => {
element.addEventListener(evtType, this.handleClick_ as EventListener);
Copy link
Contributor

Choose a reason for hiding this comment

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

since element can potentially be null, I think we need a guard here.

Copy link
Contributor

Choose a reason for hiding this comment

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

The component is only valid if nativeInput or selectedText is present, and the code above declares that selectedText will be defined if nativeInput isn't (there's also a comment above 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.

element should never be null:

// One of these elements must be non-null.
const element = this.nativeControl_ || this.selectedText_!;

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we then throw an error if both are missing? It seems like a helpful user error to know why the component wouldn't be working. I guess it is obvious if you don't have a select/list, just throwing out ideas.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that sounds like a good idea. I'll do that and add a unit test.

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

packages/mdc-select/index.ts Outdated Show resolved Hide resolved
packages/mdc-select/index.ts Outdated Show resolved Hide resolved
*/
private getNormalizedXCoordinate_(evt: MouseEvent): number {
const targetClientRect = (evt.target as Element).getBoundingClientRect();
const xCoordinate = evt.clientX; // TODO(acdvorak): How should this be typed?
Copy link
Contributor

Choose a reason for hiding this comment

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

isn't this inferred to be a number already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The problem is that this method actually takes a MouseEvent | TouchEvent, and clientX isn't defined on TouchEvent. I'm actually not sure how this code works at all right now.

type PointerEventType = 'mousedown' | 'touchstart';

const POINTER_EVENTS: PointerEventType[] = ['mousedown', 'touchstart'];

class MDCSelect {
  private handleClick_!: SpecificEventListener<PointerEventType>; // assigned in initialize()

  initialSyncWithDOM() {
    this.handleClick_ = (evt) => {
      if (this.selectedText_) {
        this.selectedText_.focus();
      }
      this.foundation_.handleClick(this.getNormalizedXCoordinate_(evt));
    };

    POINTER_EVENTS.forEach((evtType) => {
      element.addEventListener(evtType, this.handleClick_ as EventListener);
    });
  }

  private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number {
    const targetClientRect = (evt.target as Element).getBoundingClientRect();
    const xCoordinate = evt.clientX; // TODO(acdvorak): How should this be typed?
    return xCoordinate - targetClientRect.left;
  }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

OH! Wait I think there is a bug for this...I don't think it works.

Copy link
Contributor

Choose a reason for hiding this comment

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

Having a hard time finding it, but I do believe this was an issue in either React or Web or both.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Didn't we fix this somewhere to dig out touches[0], which does have clientX?

Copy link
Contributor

Choose a reason for hiding this comment

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

The above issue I linked to is text field. So I think we also need to fix for select.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call! A type guard should do the trick:

class MDCSelect {
  private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number {
    const targetClientRect = (evt.target as Element).getBoundingClientRect();
    const xCoordinate = this.isTouchEvent_(evt) ? evt.touches[0].clientX : evt.clientX;
    return xCoordinate - targetClientRect.left;
  }

  private isTouchEvent_(evt: MouseEvent | TouchEvent): evt is TouchEvent {
    return Boolean((evt as TouchEvent).touches);
  }
}

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

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit ebbbbb9 vs. feat/typescript! 💯🎉

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit a236050 vs. feat/typescript! 💯🎉

@@ -336,14 +239,111 @@ class MDCSelect extends MDCComponent<MDCSelectFoundation> implements RippleCapab
super.destroy();
}

get value(): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

wow I wish Github would just move the initialize method lol

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit 1e8b7e2 vs. feat/typescript! 💯🎉

private nativeControl_!: HTMLSelectElement | null; // assigned in initialize()
private selectedText_!: HTMLElement | null; // assigned in initialize()

private targetElement_!: HTMLElement; // 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.

good clean up idea!

@mdc-web-bot
Copy link
Collaborator

All 624 screenshot tests passed for commit a0cce8c vs. feat/typescript! 💯🎉

return xCoordinate - targetClientRect.left;
}

private isTouchEvent_(evt: MouseEvent | TouchEvent): evt is TouchEvent {
Copy link
Contributor

Choose a reason for hiding this comment

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

what if we changed this to just getXCoordinateFromEvent? Then we just encapsulate this this.isTouchEvent_(evt) ? evt.touches[0].clientX : evt.clientX; logic...cause we still end up having to cast it anyways inside this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed offline, it would need to look like this:

  private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number {
    const targetClientRect = (evt.target as Element).getBoundingClientRect();
    return this.getClientXFromEvent_(evt) - targetClientRect.left;
  }

  private getClientXFromEvent_(evt: MouseEvent | TouchEvent): number {
    const touchEvt = evt as TouchEvent;
    const mouseEvt = evt as MouseEvent;
    if (touchEvt.touches) {
      return touchEvt.touches[0].clientX;
    }
    return mouseEvt.clientX;
  }

Matt and I prefer the isTouchEvent_() style, so we're going to leave it as-is.

Copy link
Contributor

@moog16 moog16 left a comment

Choose a reason for hiding this comment

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

Looks good!

@acdvorak acdvorak dismissed kfranqueiro’s stale review February 13, 2019 00:17

Addressed Ken's comments; approved by Matt

@acdvorak acdvorak merged commit 5052ada into feat/typescript Feb 13, 2019
@acdvorak acdvorak deleted the feat/typescript--select branch February 13, 2019 00:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants