Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 76 additions & 1 deletion docs/content/docs/3.composables/use-overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ Create an overlay, and return a factory instance.
::
::

### on()

`on(event: string, callback: (value: unknown) => void): void`{lang="ts-type"}

#### Parameters

::field-group
::field{name="event" type="string" required}
The event to listen to.
::

::field{name="callback" type="(value: unknown) => void" required}
The callback to invoke when the event is emitted.
::
::

::warning
Emits and its callback arguments are infer from component but it's not working for component which have more than 5 emits.
::

### open()

`open(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T>`{lang="ts-type"}
Expand Down Expand Up @@ -158,6 +178,43 @@ In-memory list of all overlays that were created.

## Instance API

### on()

`on(event: string, callback: (value: unknown) => void): void`{lang="ts-type"}

Listen to overlay's component emits.

#### Parameters

::field-group
::field{name="event" type="string" required}
The event to listen to.
::

::field{name="callback" type="(value: unknown) => void" required}
The callback to invoke when the event is emitted.
::
::

```vue
<script setup lang="ts">
import { LazyModalExample } from '#components'

const overlay = useOverlay()

const modal = overlay.create(LazyModalExample)
modal.on('event-emitted', () => {
console.log('event emitted')
})

function openModal() {
modal.open({
title: 'Welcome'
})
}
</script>
```

### open()

`open(props?: ComponentProps<T>): Promise<OpenedOverlay<T>>`{lang="ts-type"}
Expand Down Expand Up @@ -252,15 +309,27 @@ const modalB = overlay.create(ModalB)

const slideoverA = overlay.create(SlideoverA)

modalA.on('close', (value) => {
console.log(value)
})

const openModalA = () => {
// Open modalA, but override the title prop
modalA.open({ title: 'Hello' })
}

const openModalB = async () => {
// Open modalB, and wait for its result
// Open modalB
const modalBInstance = modalB.open()

/// Open the slideover when modalB closes
/// - Using the callback argument of the `on` method
modalB.on('close', (input) => {
slideoverA.open({ input })
})

/// - Using the promise returned by `open`
// Wait for opening result (resolved automatically when the overlay is closed)
const input = await modalBInstance

// Pass the result from modalB to the slideover, and open it
Expand Down Expand Up @@ -297,3 +366,9 @@ const modal = overlay.create(LazyModalExample, {
})
</script>
```

### Limitation for components with more than 5 emits

Because of TypeScript limitation for infer overloaded functions in conditional types (cf: https://github.com/microsoft/TypeScript/issues/32164).

Because of this limitation, you could expect typing issues for components with more than 5 defined emits. This limitation is set 5 as we use a trick to make work for most cases.
6 changes: 1 addition & 5 deletions src/runtime/components/OverlayProvider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ const onAfterLeave = (id: symbol) => {
close(id)
unmount(id)
}

const onClose = (id: symbol, value: any) => {
close(id, value)
}
</script>

<template>
Expand All @@ -24,7 +20,7 @@ const onClose = (id: symbol, value: any) => {
:key="overlay.id"
v-bind="overlay.props"
v-model:open="overlay.isOpen"
@close="(value:any) => onClose(overlay.id, value)"
v-on="overlay.emits"
@after:leave="onAfterLeave(overlay.id)"
/>
</template>
71 changes: 65 additions & 6 deletions src/runtime/composables/useOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,45 @@ import { reactive, markRaw, shallowReactive } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import type { ComponentProps, ComponentEmit } from 'vue-component-type-helpers'

/**
* Workaround for TypeScript limitation with overloaded functions in conditional types.
*
* TypeScript's conditional types infer from the last overload only when matching overloaded functions.
* These utilities extract the union of all event names and their corresponding arguments from ComponentEmit<T>'s overloads.
*
* @see https://github.com/microsoft/TypeScript/issues/32164
*/
type OverloadUnion<T> = T extends {
(...args: infer A1): any
(...args: infer A2): any
(...args: infer A3): any
(...args: infer A4): any
} ? A1[0] | A2[0] | A3[0] | A4[0] : T extends {
(...args: infer A1): any
(...args: infer A2): any
(...args: infer A3): any
} ? A1[0] | A2[0] | A3[0] : T extends {
(...args: infer A1): any
(...args: infer A2): any
} ? A1[0] | A2[0] : T extends (...args: infer A1) => any ? A1[0] : never

type OverloadArgs<T, K> = T extends {
(event: K, ...args: infer A1): any
(...args: any[]): any
(...args: any[]): any
(...args: any[]): any
} ? A1 : T extends {
(event: K, ...args: infer A1): any
(...args: any[]): any
(...args: any[]): any
} ? A1 : T extends {
(event: K, ...args: infer A1): any
(...args: any[]): any
} ? A1 : T extends (event: K, ...args: infer A1) => any ? A1 : never

type EmitKeys<T> = OverloadUnion<ComponentEmit<T>>
type EmitArgs<T, K> = OverloadArgs<ComponentEmit<T>, K>

/**
* This is a workaround for a design limitation in TypeScript.
*
Expand Down Expand Up @@ -33,7 +72,7 @@ type CloseEventArgType<T> = T extends {
} ? Arg : never
export type OverlayOptions<OverlayAttrs = Record<string, any>> = {
defaultOpen?: boolean
props?: OverlayAttrs
props?: Partial<OverlayAttrs>
destroyOnClose?: boolean
}

Expand All @@ -42,19 +81,21 @@ interface ManagedOverlayOptionsPrivate<T extends Component> {
id: symbol
isMounted: boolean
isOpen: boolean
emits?: Record<string, (...args: any[]) => unknown>
originalProps?: ComponentProps<T>
resolvePromise?: (value: any) => void
}
export type Overlay = OverlayOptions<Component> & ManagedOverlayOptionsPrivate<Component>

type OverlayInstance<T extends Component> = Omit<ManagedOverlayOptionsPrivate<T>, 'component'> & {
type OverlayInstance<T extends Component> = Omit<ManagedOverlayOptionsPrivate<T>, 'component' | 'emits'> & {
id: symbol
open: (props?: ComponentProps<T>) => OpenedOverlay<T>
close: (value?: any) => void
patch: (props: Partial<ComponentProps<T>>) => void
on<K extends EmitKeys<T>>(event: K, callback: (...args: EmitArgs<T, K>) => void): void
}

type OpenedOverlay<T extends Component> = Omit<OverlayInstance<T>, 'open' | 'close' | 'patch' | 'modelValue' | 'resolvePromise'> & {
type OpenedOverlay<T extends Component> = Omit<OverlayInstance<T>, 'open' | 'close' | 'patch' | 'modelValue' | 'resolvePromise' | 'on'> & {
result: Promise<CloseEventArgType<ComponentEmit<T>>>
} & Promise<CloseEventArgType<ComponentEmit<T>>>

Expand All @@ -64,13 +105,18 @@ function _useOverlay() {
const create = <T extends Component>(component: T, _options?: OverlayOptions<ComponentProps<T>>): OverlayInstance<T> => {
const { props, defaultOpen, destroyOnClose } = _options || {}

const id = Symbol(import.meta.dev ? 'useOverlay' : '')

const options = reactive<Overlay>({
id: Symbol(import.meta.dev ? 'useOverlay' : ''),
id,
isOpen: !!defaultOpen,
component: markRaw(component!),
isMounted: !!defaultOpen,
destroyOnClose: !!destroyOnClose,
originalProps: props || {},
emits: {
close: (value: unknown) => close(id, value)
},
props: { ...props }
})

Expand All @@ -80,7 +126,8 @@ function _useOverlay() {
...options,
open: <T extends Component>(props?: ComponentProps<T>) => open(options.id, props),
close: value => close(options.id, value),
patch: <T extends Component>(props: Partial<ComponentProps<T>>) => patch(options.id, props)
patch: <T extends Component>(props: Partial<ComponentProps<T>>) => patch(options.id, props),
on: <T extends Component, K extends EmitKeys<T>>(event: K, callback: (...args: EmitArgs<T, K>) => void): void => on(options.id, event, callback)
}
}

Expand Down Expand Up @@ -155,12 +202,24 @@ function _useOverlay() {
return overlay.isOpen
}

function on<T extends Component, K extends EmitKeys<T>>(
id: symbol,
event: K,
callback: (...args: EmitArgs<T, K>) => void
): void {
const overlay = getOverlay(id)

if (!overlay.emits) overlay.emits = {}
overlay.emits[event as string] = callback
}

return {
overlays,
create,
on,
open,
close,
closeAll,
create,
patch,
unmount,
isOpen
Expand Down
Loading
Loading