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

[Component]: adds event types to react #440

Conversation

benjaminknox
Copy link
Contributor

@benjaminknox benjaminknox commented Oct 17, 2023

Resolves #407

For react components, currently generic typing allows any event prop to be used (e.g. <LeoButton onClick={...}>), if an incorrect event name is used (e.g. <LeoButton onClicked={...}>) there is no type error. Ideally a type error for an incorrect prop name would show.

This solves this by generating an event type for the react components by parsing the corresponding svelte type file, it should contain event types resulting from the typed createEventDispatcher. When there are no events defined in the svelte file it doesn't include any event types.

For example, in input.svelte#L74-L84:

  type InputEventDetail = {
    innerEvent: Event & { target: HTMLInputElement }
    value: string
    valueAsNumber: number
    valueAsDate: number
  }

  const dispatch = createEventDispatcher<{
    change: InputEventDetail
    input: InputEventDetail
    focus: InputEventDetail
    blur: InputEventDetail
    keydown: InputEventDetail
    keyup: InputEventDetail
    keypress: InputEventDetail
    focusin: InputEventDetail
    focusout: InputEventDetail
  }>()

Results in a type definition that looks like this:

import type * as React from 'react'
import type { ReactProps } from '../src/components/svelte-react'
import type { InputEvents as SvelteEvents, InputProps as SvelteProps } from '../types/components/input/input.svelte';

export type InputEventProps = {
  onChange?: (event: CustomEvent<{
      innerEvent: Event & {
          target: HTMLInputElement;
      };
      value: string;
      valueAsNumber: number;
      valueAsDate: number;
  }) => void;
  onInput?: (event: CustomEvent<{...}>) => void;
  onFocus?: (event: CustomEvent<{...}>) => void;
  onBlur?: (event: CustomEvent<{...}>) => void;
  onKeydown?: (event: CustomEvent<{...}>) => void;
  onKeyup?: (event: CustomEvent<{...}>) => void;
  onKeypress?: (event: CustomEvent<{...}>) => void;
  onFocusin?: (event: CustomEvent<{...}>) => void;
  onFocusout?: (event: CustomEvent<{...}>) => void
}

export type InputProps = InputEventProps & ReactProps<SvelteProps, SvelteEvents>;
export default function InputReact(props: React.PropsWithChildren<InputProps>): JSX.Element

// As we don't currently have type definitions for the web components, export
// the Type Definitions from the Svelte component.
export * from '../types/components/input/input.svelte'
Screen.Recording.2023-10-17.at.1.17.50.PM.mov

@benjaminknox benjaminknox force-pushed the bpk/show-compile-errors-on-events-in-react branch from 9c6748d to 5d8b1c1 Compare October 17, 2023 18:15
Comment on lines 15 to 24
return `
export type ${componentName}EventProps = {
${componentEventNames.map((event) => ` ${event}?: (event: CustomEvent) => void`).join(';\n')}
}
`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Creates a type that gets associated with the component type during the generation of the react components.

Copy link
Collaborator

@fallaciousreasoning fallaciousreasoning left a comment

Choose a reason for hiding this comment

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

Wow thanks for this @benjaminknox - I'm going to take a couple of days to mull it over (and I'm away the rest of this week) but this seems like a sensible approach.

The main thing we might be missing is the ability to accept standard HTML events. When we wrap these in a WebComponent, it actually supports all the standard events (onClick, onMouseEnter, onMouseDown, onKeyDown ect), so we should make sure we are still able to accept those events too

@benjaminknox
Copy link
Contributor Author

benjaminknox commented Oct 18, 2023

Note: The method below is not preferred and the PR it references is closed because the html events are not available yet, see this comment: benjaminknox#1 (comment)

The main thing we might be missing is the ability to accept standard HTML events. When we wrap these in a WebComponent, it actually supports all the standard events (onClick, onMouseEnter, onMouseDown, onKeyDown ect), so we should make sure we are still able to accept those events too

Thanks @fallaciousreasoning, I think I have a possible solution for this, the details are in a PR I created against this branch in my fork of leo: benjaminknox#1

Basically, the solutions works when a component imports and uses SvelteHTMLElements in the svelte file, for instance if I were to add a table component:

<script lang="ts">
  import type { SvelteHTMLElements } from 'svelte/elements'

  type TableProps = Partial<SvelteHTMLElements['table']>

  type $$Props = TableProps

  let newValue: string = ''

  let table: HTMLTableElement = undefined
</script>

<table bind:this={table} {...$$restProps}><slot /></table>

The resulting react type file would be:

import type * as React from 'react'
import type { ReactProps } from '../src/components/svelte-react'
import type { TableEvents as SvelteEvents, TableProps as SvelteProps } from '../types/components/table/table.svelte';


export type TableProps = TableEventProps & ReactProps<SvelteProps, SvelteEvents>;
export default function TableReact(props: React.PropsWithChildren<React.HTMLAttributes<HTMLElement | HTMLTableElement> | TableProps>): JSX.Element

// As we don't currently have type definitions for the web components, export
// the Type Definitions from the Svelte component.
export * from '../types/components/table/table.svelte'

@benjaminknox benjaminknox force-pushed the bpk/show-compile-errors-on-events-in-react branch from e90e7e2 to 8af2e2d Compare October 23, 2023 15:51
@benjaminknox
Copy link
Contributor Author

@fallaciousreasoning I think I've addressed all of your comments here.

Although there's one more thing bothering me, we end up with event names that are not quite camelcase, like in the Input component in the example above there is onKeypress instead of the react onKeyPress.

Is this ok to keep this way?

I have a few ideas on how to fix this:

  1. We could implement something like this library to parse the words in the event name.
  2. We could limit it to the events that are standard on react elements and match those event names case insensitively, e.g. onKeydown => onKeyDown, onMouseleave => onMouseLeave, or onFocusout => onFocusOut. (leaving any non-standard events)

Any thoughts?

@fallaciousreasoning
Copy link
Collaborator

Hmm, you're right, that's not ideal - shall we see if we can solve it? I think I'd be pretty happy with a solution which just handles the standard HTML events, which we can probably do as a Typescript-only fix:

image
(my super hacky PoC)

type EventNames = 'onMouseDown' | 'onMouseEnter' | 'onClick' | 'onKeyDown' | 'onKeyUp'
type EventNameMap = {
  [P in EventNames as Lowercase<P>]: P
}

type Events<T> = {
  [P in keyof T as (P extends Lowercase<EventNames> ? EventNameMap[P] : P)]: T[P]
} 

let f: Events<{ 'onclick': () => {}, 'ondidthething': () => {}, onmouseenter: () => {} }>
f.<CTRL+Space>

@benjaminknox benjaminknox force-pushed the bpk/show-compile-errors-on-events-in-react branch from b6f0fbd to 17ea850 Compare October 31, 2023 14:52
@benjaminknox benjaminknox force-pushed the bpk/show-compile-errors-on-events-in-react branch from 1ad276f to e8f9a50 Compare November 11, 2023 11:45
@fallaciousreasoning
Copy link
Collaborator

fallaciousreasoning commented Nov 12, 2023

@benjaminknox I finally got around to testing this out in https://github.com/brave/brave-core, which is our main consumer of this repo. This is mostly working great but I did notice a couple of things:

  1. On the LeoButton component, the event names which are forwarded to the internal button are named on:<eventname> instead of on<Eventname> I just noticed it was already acting like this 😆
  2. We seem to have lost the uppercase letter at the start of the event name (i.e. onClose is now onclose)

I think if we fix these it should be good to land 😄

for capitalizing on in front of event names I think this should work:

type Events = ['onclick', 'onclose', 'onopen', 'onchange', 'oninput']

type CasedBetter<T> = T extends `on${infer K}` ? `on${Capitalize<K>}` : never

type CapitalOn = CasedBetter<Events[number]>
// type CapitalOn = "onClick" | "onClose" | "onOpen" | "onChange" | "onInput"

@fallaciousreasoning
Copy link
Collaborator

@benjaminknox I pushed a commit to fix the event names 😄

@fallaciousreasoning fallaciousreasoning force-pushed the bpk/show-compile-errors-on-events-in-react branch from 905fa2a to 79a8627 Compare November 13, 2023 00:28
Copy link
Collaborator

@fallaciousreasoning fallaciousreasoning left a comment

Choose a reason for hiding this comment

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

This LGTM - if you sign the commits I can land it 😄

benjaminknox and others added 2 commits November 12, 2023 21:39
remove lodash.capitalize dependency

define types on createEventDispatcher in place of CustomEvent

adds warning when finding an untyped createEventDispatcher

run format

add event detail type

adds back event type to dialog dispatch

matches react type camelcase for standard events
@benjaminknox benjaminknox force-pushed the bpk/show-compile-errors-on-events-in-react branch from 79a8627 to 7a3dcba Compare November 13, 2023 02:39
@benjaminknox
Copy link
Contributor Author

This LGTM - if you sign the commits I can land it 😄

Done!

We seem to have lost the uppercase letter at the start of the event name (i.e. onClose is now onclose)

Good catch thanks @fallaciousreasoning

@fallaciousreasoning fallaciousreasoning merged commit 1be4d61 into brave:main Nov 13, 2023
@fallaciousreasoning
Copy link
Collaborator

Thanks heaps for this @benjaminknox!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Figure out how we can have compile errors when react is using the wrong event name
2 participants