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
9 changes: 5 additions & 4 deletions packages/0/src/components/Portal/Portal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
}>()

const {
to = 'body',
to,
disabled = false,
blocking = false,
scrim = true,
Expand All @@ -71,9 +71,10 @@
})
ticket.select()

const target = toRef(() => (
to === 'top-layer' ? stack.topElement.value ?? 'body' : to
))
const target = toRef(() => {
const resolvedTo = to ?? stack.default.value ?? 'body'
return resolvedTo === 'top-layer' ? stack.topElement.value ?? 'body' : resolvedTo
})

const slotProps = toRef((): PortalSlotProps => ({
zIndex: ticket.zIndex.value,
Expand Down
50 changes: 50 additions & 0 deletions packages/0/src/composables/useStack/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ describe('createStack', () => {
expect(ticket1.id).not.toBe(ticket2.id)
})

it('should expose default teleport from options', () => {
const stack = createStack({ default: 'top-layer' })
expect(stack.default.value).toBe('top-layer')
})

it('should have undefined default when not set', () => {
const stack = createStack()
expect(stack.default.value).toBeUndefined()
})

it('should handle toggle', async () => {
const stack = createStack()
const ticket = stack.register()
Expand Down Expand Up @@ -319,6 +329,46 @@ describe('createStack', () => {

expect(contextFound).toBe(true)
})

it('should expose default teleport option via stack.default', () => {
const plugin = createStackPlugin({ default: 'top-layer' })

let defaultValue: string | HTMLElement | undefined

const TestComponent = defineComponent({
setup () {
const ctx = useStack()
defaultValue = ctx.default.value
return () => null
},
})

mount(TestComponent, {
global: { plugins: [plugin] },
})

expect(defaultValue).toBe('top-layer')
})

it('should have undefined default when no default option is given', () => {
const plugin = createStackPlugin()

let defaultValue: string | HTMLElement | undefined = 'sentinel'

const TestComponent = defineComponent({
setup () {
const ctx = useStack()
defaultValue = ctx.default.value
return () => null
},
})

mount(TestComponent, {
global: { plugins: [plugin] },
})

expect(defaultValue).toBeUndefined()
})
})

describe('useStack', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/0/src/composables/useStack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { onScopeDispose, toRef, toValue } from 'vue'
// Types
import type { SelectionContext, SelectionOptions, SelectionTicket, SelectionTicketInput } from '#v0/composables/createSelection'
import type { ContextTrinity } from '#v0/composables/createTrinity'
import type { Extensible } from '#v0/types'
import type { App, ComputedRef, MaybeRefOrGetter, Ref } from 'vue'

/**
Expand Down Expand Up @@ -160,6 +161,11 @@ export interface StackContext<
* and scrim clicks should be ignored.
*/
isBlocking: Readonly<Ref<boolean>>
/**
* App-wide default teleport target, as set via `createStackPlugin({ default: … })`.
* Portal reads this when no per-component `to` prop is given.
*/
default: Readonly<Ref<Extensible<'top-layer'> | HTMLElement | undefined>>
/**
* Element of the topmost selected modal overlay that has a resolvable DOM
* element (a top-layer `<dialog>`), or `null`.
Expand Down Expand Up @@ -210,6 +216,20 @@ export interface StackOptions extends SelectionOptions {
* overlay-internal elements (dropdowns, tooltips within dialogs).
*/
increment?: number
/**
* App-wide default teleport target for all Portals.
*
* @remarks When set, every Portal that has no explicit `to` prop inherits
* this target. A per-component `to` prop always takes precedence.
* `'top-layer'` resolves to the topmost open modal element (a `<dialog>`),
* falling back to `'body'` when no modal is active.
*
* @example
* ```ts
* app.use(createStackPlugin({ default: 'top-layer' }))
* ```
*/
default?: Extensible<'top-layer'> | HTMLElement
}

export interface StackContextOptions extends StackOptions {
Expand Down Expand Up @@ -247,6 +267,7 @@ export function createStack (_options: StackOptions = {}): StackContext {
const {
baseZIndex = 2000,
increment = 10,
default: defaultTeleport,
...options
} = _options

Expand Down Expand Up @@ -333,6 +354,8 @@ export function createStack (_options: StackOptions = {}): StackContext {
return ticket as StackTicket
}

const defaultRef = toRef(() => defaultTeleport)

return {
...selection,
register,
Expand All @@ -341,6 +364,7 @@ export function createStack (_options: StackOptions = {}): StackContext {
scrimZIndex,
isBlocking,
topElement,
default: defaultRef,
get size () {
return selection.size
},
Expand Down
Loading