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

Initial commit of typescript type definitions for EUI components & services #252

Merged
merged 7 commits into from
Jan 10, 2018
Merged

Initial commit of typescript type definitions for EUI components & services #252

merged 7 commits into from
Jan 10, 2018

Conversation

uboness
Copy link
Contributor

@uboness uboness commented Dec 28, 2017

relates to #256

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

This is a great start 👍 There are some aspects which we might want to make a conscious decision about though:

component type definitions

There is an alternative to declaring classes with empty bodies. Instead one could write

export const EuiButton: React.SFC<EuiButtonProps>;

The differences I can see are the following:

  • it does not define a new type, it just assigns a type to the export
  • it does not suggest that the component as a state of type {}
  • it does not tempt the author to put something into the class body, but instead restricts him to declaring the prop types

I have not arrived at a conclusion about which style is better. The const style appeals to me due to its simplicity though.

inline unions

In some places the prop types are defined and exported separately (e.g. ButtonColor) and in some places they are defined inline in the props interface. Do we want to stick to one style or set some rule about which style to apply when?

common props

There are some props like className, aria-label or data-test-subject, which most components have to support. So we might want to deduplicate them and mix them into the prop type definition using intersection types:

interface CommonProps {
  className?: string;
  'aria-label'?: string;
  'data-test-subj'?: string;
}

interface AnyProps {
  [prop: string]: any;
}

interface EuiButtonProps {
  /* ... */
}

const EuiButton: React.SFC<
  CommonProps &
    AnyProps &
    EuiButtonProps
>;

pass-through props

If we know that the "rest" of the props are passed through to a specific child component, we could reflect that in the type to avoid unchecked props and missing autocompletion:

export const EuiButton: React.SFC<
  CommonProps &
    AnyProps &
    React.ButtonHTMLAttributes<HTMLButtonElement> &
    EuiButtonProps
>;

@uboness
Copy link
Contributor Author

uboness commented Jan 2, 2018

component type definitions

I wan't aware of React.SFC<P>... this seems cleaner to me

inline unions

I prefer exporting props separately (it's more open for reuse and extensions)

common props & pass-through props

++ to both

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

Another idea we might want to consider is avoiding the repitition of the ambient module definition in every .d.ts file. Instead using normal export chain in those files should work too (at least it did when I last tested):

// in src/components/badge/index.d.ts
export const EuiBadge: React.SFC<EuiBadgeProps>;
// in src/components/index.d.ts
export * from './components/badge';
// in src/index.d.ts
export * from './components';

@uboness
Copy link
Contributor Author

uboness commented Jan 2, 2018

@weltenwort updated based on your proposals. I tried to keep it as close as possible to the props that are supported in the js files... I find them to be quite inconsistent when it comes to the ...rest usage (it's not always clear what elements these will be applied to)... no the less, I just followed the code.

Another idea we might want to consider is avoiding the repitition of the ambient module definition in every .d.ts file. Instead using normal export chain in those files should work too (at least it did when I last tested):

strange... I've never seen this sort of import working within type definitions. I'm inclined to keep using the triple slash directive as this (as far as I know) is the official way you should direct the compiler to other type definitions. Aside from that, personally I don't mind the repetition of the module declaration - the nice side effect is that when you jump to it and you look at the file, you immediately know the module it belongs to.

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

I had a closer look at the code and checked it with the compiler. Aside from the individual comments below a few aspects apply to multiple files:

  • The compiler complains about imports inside the module augmentations, which can be easily fixed by moving them to the outer scope.

  • How about using React.HTMLAttributes instead of React.DOMAttributes? They provide a more complete set of properties.

  • Do we maybe want to apply prettier to the files (similar to the logging and new platform codebase)?

I can prepare a PR to this PR with those changes if you prefer.


declare module '@elastic/eui' {

import { SFC, ButtonHTMLAttributes, MouseEventHandler } from 'react';
Copy link
Member

Choose a reason for hiding this comment

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

This shows an error for me (imports in module augmentations not allowed). How about moving this to line 3?


export interface EuiButtonProps {
onClick: MouseEventHandler<HTMLButtonElement>; //overriding DOMAttributes to make this required
iconType?: IconType,
Copy link
Member

Choose a reason for hiding this comment

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

A /// <reference path="../icon/index.d.ts" /> at the top is required for this to resolve correctly.

CommonProps,
ButtonHTMLAttributes<HTMLButtonElement>,
EuiButtonEmptyProps
>;
Copy link
Member

Choose a reason for hiding this comment

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

You probably meant to use & instead of , here.


export type EuiContextMenuPanel = SFC<
CommonProps &
Omit<DOMAttributes<HTMLDivElement>, 'ref', 'onKeyDown', 'tabIndex', 'onAnimationEnd'> &
Copy link
Member

Choose a reason for hiding this comment

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

How about we use HTMLAttributes a more complete interface?

Apart from that, the second argument to Omit should be a union type, i.e. 'ref' | 'onKeyDown' | 'tabIndex' | 'onAnimationEnd'.


export type EuiContextMenuItem = SFC<
CommonProps &
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type', 'ref'> &
Copy link
Member

Choose a reason for hiding this comment

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

Same as above (| instead of ,) and the ButtonHTMLAttributes don't include ref in the first place.


export type EuiPaginationButton = SFC<
CommonProps &
Omit<EuiButtonEmptyProps, 'size', 'color'> &
Copy link
Member

Choose a reason for hiding this comment

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

a union type is required here as well


export type EuiPanel = SFC<
CommonProps &
Omit<DOMAttributes<HTMLDivElement>, 'ref'> &
Copy link
Member

Choose a reason for hiding this comment

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

ref is not included in DOMAttributes (or HTMLAttributes)

ownFocus?: boolean,
anchorPosition?: PopoverAnchorPosition,
panelClassName?: string,
panelPaddingSize?: PanelPaddingSize,
Copy link
Member

Choose a reason for hiding this comment

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

This requires a

/// <reference path="../panel/index.d.ts" />

at the top.

*/

export interface EuiTableHeaderButtonProps {
iconType?: IconType
Copy link
Member

Choose a reason for hiding this comment

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

This requires

/// <reference path="../icon/index.d.ts" />

export type EuiTableRowCellCheckbox = SFC<
CommonProps &
TdHTMLAttributes<HTMLTableCellElement> &
EuiTableRowCellCheckbox
Copy link
Member

Choose a reason for hiding this comment

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

This should probably mean EuiTableRowCellCheckboxProps.

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

I just noticed that the components are only exported as types, but not values. Instead of the export type EuiButton = SFC<...> we need export const EuiButton: SFC<...> so they are actually defined in the value scope.

@uboness
Copy link
Contributor Author

uboness commented Jan 3, 2018

I just noticed that the components are only exported as types, but not values. Instead of the export type EuiButton = SFC<...> we need export const EuiButton: SFC<...> so they are actually defined in the value scope.

good catch... I guess that was implicit with the class exports before... will fix that

@uboness
Copy link
Contributor Author

uboness commented Jan 3, 2018

@weltenwort updated

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

Just a few more points I ran into while trying to use these definitions.

export const EuiPopover: SFC<
CommonProps &
HTMLAttributes<HTMLDivElement> &
EuiPanelProps
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be EuiPopoverProps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

export type EuiContextMenuPanelId = string | number;

export interface EuiContextMenuProps {
panels?: EuiContextMenuPanel[],
Copy link
Member

Choose a reason for hiding this comment

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

I ran into problems when trying to use this. Turns out the panels data structure here is not the React component, but an array of objects describing the panels. I wrote a working definition for the KUI context menu a while back, which should still be compatible:

  interface ContextMenuPanelItem
    extends React.ButtonHTMLAttributes<HTMLButtonElement> {
    name: string;
    icon?: React.ReactNode;
    panel?: string | number;
  }

  interface ContextMenuPanel {
    id: string | number;
    title: string;
    items?: ContextMenuPanelItem[];
    content?: React.ReactNode;
  }

  export const KuiContextMenu: React.SFC<
    CommonProps &
      AnyProps & {
        initialPanelId: string | number;
        panels?: ContextMenuPanel[];
      }
  >;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

pfff... yea.. can you check these ones:

export type EuiContextMenuPanelId = string | number;


  interface EuiContextMenuPanelItem extends EuiContextMenuItemProps {
    name: string;
    panel?: EuiContextMenuPanelId;
  }

  interface EuiContextMenuPanelDescriptor {
    id: EuiContextMenuPanelId,
    title: string;
    items?: EuiContextMenuPanelItem[];
    content?: React.ReactNode;
  }

  export interface EuiContextMenuProps {
    panels?: EuiContextMenuPanelDescriptor[],
    initialPanelId?: EuiContextMenuPanelId
  }

  export const EuiContextMenu: SFC<
    Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'style'> &
    EuiContextMenuProps
    >;

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that seems to work. Although I don't think it is correct for EuiContextMenuPanelItem (which could be called EuiContextMenuItemDescriptor to clarify the symmetry) to extend EuiContextMenuItemProps. It should not contain the hasPanel and onClick properties, for example (see

const {
panel,
name,
icon,
onClick,
...rest
} = item;
const onClickHandler = panel
? () => {
// This component is commonly wrapped in a EuiOutsideClickDetector, which means we'll
// need to wait for that logic to complete before re-rendering the DOM via showPanel.
window.requestAnimationFrame(() => {
if (onClick) onClick();
this.showNextPanel(index);
});
} : onClick;
return (
<EuiContextMenuItem
key={name}
icon={icon}
onClick={onClickHandler}
hasPanel={Boolean(panel)}
{...rest}
>
{name}
</EuiContextMenuItem>
);
).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no fully accurate... the onClick is actually an optional prop on the descriptor as well (just like it's optional on the item itself). The only difference here is the hasPanel vs. panel. The ...rest` are passthrough props to the item itself as well. I think extending it makes sense to some degree, with perhaps a small modification:

export type EuiContextMenuPanelItemDescriptor = Omit<EuiContextMenuItemProps, 'hasPanel'> & {
    name: string;
    panel?: EuiContextMenuPanelId;
  };

wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

true... I guess what threw me off with the onClick is the fact that

if (onClick) onClick();
changes the signature from (event: MouseEvent<T>) => void to () => void. But that's a bug with the original js file. I'll create a PR for that.

Copy link
Member

Choose a reason for hiding this comment

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

The PR is #265.

Copy link
Member

@weltenwort weltenwort left a comment

Choose a reason for hiding this comment

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

I think this is almost ready. Only found a few things remain (see comments below). I couldn't test every prop of every component, so I expect some detail adjustments might be necessary when the definitions are used more comprehensively.

Finally, how do we want to handle the formatting? I would be in favor of applying prettier to all definition files to remove any ambiguity.

* @see './context_menu_panel.js`
*/

import { HTMLAttributes } from 'react';
Copy link
Member

Choose a reason for hiding this comment

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

This still throws an error for me. Could probably be removed because line 3 already contains it.

export type ButtonSize = 's' | 'l';

export interface EuiButtonProps {
onClick: MouseEventHandler<HTMLButtonElement>; //overriding DOMAttributes to make this required
Copy link
Member

Choose a reason for hiding this comment

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

Should the comment refer to ButtonHTMLAttributes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's actually defined in DOMAttributes

export type ButtonIconColor = 'primary' | 'danger' | 'disabled' | 'ghost' | 'text';

export interface EuiButtonIconProps {
onClick: MouseEventHandler<HTMLButtonElement>; //overriding DOMAttributes to make this required
Copy link
Member

Choose a reason for hiding this comment

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

Should the comment refer to ButtonHTMLAttributes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

same here

@uboness
Copy link
Contributor Author

uboness commented Jan 9, 2018

@weltenwort updated... left the comments as they are (they seem to be very logical to me)

@weltenwort
Copy link
Member

What do you think about the formatting I proposed?

@uboness
Copy link
Contributor Author

uboness commented Jan 9, 2018

What do you think about the formatting I proposed?

I think it makes sense to keep all our TS code consistent across the projects.... unfortunately, I think I'll have to ask you to do that though as my knowledge of build tools and such converges to zero. We can also do it in a separate PR

@weltenwort
Copy link
Member

I updated this PR with the new formatting.

uboness and others added 7 commits January 10, 2018 18:39
- introduced `CommonProps`
- introduced `AnyProps`
- introduced `NoArgCallback<T>`
- introduced `RefCallback<T>`
- introduced utility types `Diff` and `Omit` (TS's counterpart for `Pick`)
- changed all component exports to value exports
- changed to use HTMLAttributes instead of DOMAttributes
- added missing reference directives
- moved all imports to sit outside of the module declaration
- fixed use of `,` instead of `|` and `&`
- fixed the context menu definitions to better reflect the props contract
- fixed misplaced import
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants