Skip to content

Commit

Permalink
[Component]: adds event types to react (#440)
Browse files Browse the repository at this point in the history
Co-authored-by: Jay Harris <[email protected]>
  • Loading branch information
benjaminknox and fallaciousreasoning authored Nov 13, 2023
1 parent fc27a5d commit 1be4d61
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 15 deletions.
4 changes: 3 additions & 1 deletion src/components/button/button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
$: tag = href ? 'a' : ('button' as 'a' | 'button')
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
click: {}
}>()
/**
* Optional click handler
Expand Down
5 changes: 4 additions & 1 deletion src/components/checkbox/checkbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
export let isDisabled = false
export let size: Sizes = 'normal'
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
change: { checked: boolean }
}>()
function change(e) {
dispatch('change', {
checked: e.target.checked
Expand Down
5 changes: 4 additions & 1 deletion src/components/collapse/collapse.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
export let isOpen: boolean | undefined
$: isOpenInternal = isOpen ?? false
const dispatcher = createEventDispatcher()
const dispatcher = createEventDispatcher<{
toggle: { open: boolean }
}>()
const toggle = (e) => {
e.preventDefault()
Expand Down
5 changes: 4 additions & 1 deletion src/components/dialog/dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
export let backdropClickCloses = true
export let animate = true
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
close: {}
back: {}
}>()
let dialog: HTMLDialogElement
$: {
Expand Down
5 changes: 3 additions & 2 deletions src/components/dropdown/dropdown.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
export let size: Size = 'normal'
export let required = false
export let mode: Mode = 'outline'
export let showErrors = false
let dispatch = createEventDispatcher()
let dispatch = createEventDispatcher<{
change: { value: string }
}>()
let isOpen = false
let button: HTMLButtonElement
Expand Down
4 changes: 3 additions & 1 deletion src/components/radioButton/radioButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
export let isDisabled = false
const tagName = 'leo-radiobutton'
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
change: { value: string | number | any }
}>()
function changed(e) {
if (isDisabled || !e.target.checked) return
Expand Down
17 changes: 14 additions & 3 deletions src/components/svelte-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
useCallback,
type ForwardedRef,
type PropsWithChildren,
useState
useState,
type DOMAttributes
} from 'react'
import type { SvelteComponentTyped } from 'svelte'

Expand Down Expand Up @@ -35,8 +36,6 @@ export type SvelteEvents<T> = T extends SvelteComponentTyped<
? Events
: {}
export type ReactProps<Props, Events> = Props & {
[P in keyof Events as `on${Capitalize<P & string>}`]?: (e: Events[P]) => void
} & {
ref?: ForwardedRef<Partial<Props & HTMLElement> | undefined>
} & {
// Note: The div here isn't important because all props in intrinsicProps are
Expand All @@ -45,6 +44,18 @@ export type ReactProps<Props, Events> = Props & {
[P in IntrinsicProps]?: JSX.IntrinsicElements['div'][P]
}

type EventPropsNames = keyof DOMAttributes<unknown>

type EventPropsNameMap = {
[P in EventPropsNames as Lowercase<P>]: P
}
export type EventProps<T> = {
[P in keyof T as P extends Lowercase<EventPropsNames>
? EventPropsNameMap[P]
: P extends `on${infer EventName}`
? `on${Capitalize<EventName>}`
: P]: T[P]
}
const useEventHandlers = (props: any) => {
const [el, setEl] = useState<HTMLElement>()
const lastValue = useRef<{ [key: string]: (...args: any[]) => any }>({})
Expand Down
5 changes: 4 additions & 1 deletion src/components/toggle/toggle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
let dragStartX: number | undefined
let dragOffsetX: number = 0
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
change: { checked: boolean }
}>()
const onChange = (newValue?: boolean) => {
if (newValue === undefined) newValue = !checked
checked = newValue
Expand Down
4 changes: 3 additions & 1 deletion src/components/tooltip/tooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
// controlled and uncontrolled states for this component.
$: visibleInternal = visible ?? false
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher<{
visibilitychange: { visible: boolean }
}>()
let tooltip: HTMLElement
let arrow: HTMLElement
Expand Down
56 changes: 53 additions & 3 deletions src/scripts/gen-react-bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,50 @@ fs.mkdir(REACT_BINDINGS_DIRECTORY, { recursive: true })
const COMPONENT_PREFIX = 'leo'
const SVELTE_REACT_WRAPPER_PATH = '../shared/svelte-react.js'

const getEventsTypeDefinition = (
componentName,
componentEventNames,
svelteFilePath
) => `
export type ${componentName}EventProps = EventProps<{
${componentEventNames
.map(
({ eventName, detailType }) =>
` ${eventName}?: (event: ${detailType}) => void`
)
.join(';\n')}
}>
`

const findEventsTypeDefinition = async (svelteFilePath, componentName) => {
const pathToType = path.join(
__dirname,
'../../',
'types',
path.relative('./src', svelteFilePath + '.d.ts')
)
const fileContents = await fs.readFile(pathToType)

const typeRegex = /\s+([a-zA-Z0-9]+): (CustomEvent<{(\n|.)*?}>)/gm

const componentEventNames = [
...fileContents.toString().matchAll(typeRegex)
].map((match) => ({
eventName: `on${match[1]}`,
detailType: match[2]
}))

if (componentEventNames.length > 0) {
return getEventsTypeDefinition(
componentName,
componentEventNames,
svelteFilePath
)
} else {
return []
}
}

const getComponentGenerics = async (svelteFilePath, componentName) => {
const relativePath = path.relative('./src/components', svelteFilePath)
const typingsPath = path.join('./types/components', relativePath) + '.d.ts'
Expand Down Expand Up @@ -57,6 +101,10 @@ const getReactFileContents = async (svelteFilePath) => {
fileNameWithoutExtension[0].toUpperCase() +
fileNameWithoutExtension.substring(1)
const generics = await getComponentGenerics(svelteFilePath, componentName)
const eventTypeDefinition = await findEventsTypeDefinition(
svelteFilePath,
componentName
)
const hasGenerics = !!generics.length
const funcConstraints = hasGenerics
? `<${generics.map((g) => `${g.name} extends ${g.constraint}`).join(', ')}>`
Expand All @@ -77,10 +125,12 @@ export * from '../web-components/${fileNameWithoutExtension}.js'

const typeDef = `
import type * as React from 'react'
import type { ReactProps } from '../src/components/svelte-react'
import type { ReactProps, EventProps } from '../src/components/svelte-react'
import type { ${componentName}Events as SvelteEvents, ${componentName}Props as SvelteProps } from '../types/components/${containingFolder}/${fileName}';
export type ${componentName}Props${funcConstraints} = ReactProps<SvelteProps${propParams}, SvelteEvents${propParams}>;
${eventTypeDefinition && eventTypeDefinition}
export type ${componentName}Props${funcConstraints} = ${
eventTypeDefinition && `${componentName}EventProps &`
} ReactProps<SvelteProps${propParams}, SvelteEvents${propParams}>;
export default function ${componentName}React${funcConstraints}(props: React.PropsWithChildren<${componentName}Props${propParams}>): JSX.Element
// As we don't currently have type definitions for the web components, export
Expand Down

0 comments on commit 1be4d61

Please sign in to comment.