Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/app/components/search/Search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ watchDebounced(searchTerm, (term) => {
:search-status="status"
:fuse="fuse"
:transition="false"
:unmount-on-hide="false"
/>
</template>
32 changes: 32 additions & 0 deletions docs/content/docs/2.components/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,38 @@ slots:
:placeholder{class="h-48"}
::

### Unmount :badge{label="Soon" class="align-text-top"}

Use the `unmount-on-hide` prop to control whether the Modal is unmounted from the DOM when closed. Defaults to `true`.

::note
When set to `false`, the Modal's content stays in the DOM when closed (hidden with `display: none`) instead of being unmounted. This can be useful for SEO and performance by avoiding remounts on every open.
::

::component-code
---
prettier: true
ignore:
- title
props:
unmountOnHide: false
title: 'Modal mounted on hide'
slots:
default: |

<UButton label="Open" color="neutral" variant="subtle" />

body: |

<Placeholder class="h-48" />
---

:u-button{label="Open" color="neutral" variant="subtle"}

#body
:placeholder{class="h-48"}
::

### Scrollable :badge{label="4.2+" class="align-text-top"}

Use the `scrollable` prop to make the Modal's content scrollable within the overlay.
Expand Down
32 changes: 32 additions & 0 deletions docs/content/docs/2.components/slideover.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,38 @@ slots:
:placeholder{class="h-full"}
::

### Unmount :badge{label="Soon" class="align-text-top"}

Use the `unmount-on-hide` prop to control whether the Slideover is unmounted from the DOM when closed. Defaults to `true`.

::note
When set to `false`, the Slideover's content stays in the DOM when closed (hidden with `display: none`) instead of being unmounted. This can be useful for SEO and performance by avoiding remounts on every open.
::

::component-code
---
prettier: true
ignore:
- title
props:
unmountOnHide: false
title: 'Slideover mounted on hide'
slots:
default: |

<UButton label="Open" color="neutral" variant="subtle" />

body: |

<Placeholder class="h-full" />
---

:u-button{label="Open" color="neutral" variant="subtle"}

#body
:placeholder{class="h-full"}
::

## Examples

### Control open state
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
"motion-v": "^2.2.1",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"reka-ui": "2.9.8",
"reka-ui": "https://pkg.pr.new/reka-ui@fe4f7b5",
"scule": "^1.3.0",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.2.2",
Expand Down
28 changes: 14 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/runtime/components/DashboardSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { ComponentConfig } from '../types/tv'

type DashboardSearch = ComponentConfig<typeof theme, AppConfig, 'dashboardSearch'>

export interface DashboardSearchProps<T extends CommandPaletteItem = CommandPaletteItem> extends Pick<ModalProps, 'title' | 'description' | 'overlay' | 'transition' | 'content' | 'dismissible' | 'fullscreen' | 'modal' | 'portal'>, Pick<CommandPaletteProps<CommandPaletteGroup<T>, T>, 'icon' | 'trailingIcon' | 'selectedIcon' | 'childrenIcon' | 'placeholder' | 'autofocus' | 'loading' | 'loadingIcon' | 'closeIcon' | 'back' | 'backIcon' | 'disabled' | 'highlightOnHover' | 'labelKey' | 'descriptionKey' | 'preserveGroupOrder' | 'virtualize' | 'groups'> {
export interface DashboardSearchProps<T extends CommandPaletteItem = CommandPaletteItem> extends Pick<ModalProps, 'title' | 'description' | 'overlay' | 'transition' | 'content' | 'dismissible' | 'fullscreen' | 'modal' | 'portal' | 'unmountOnHide'>, Pick<CommandPaletteProps<CommandPaletteGroup<T>, T>, 'icon' | 'trailingIcon' | 'selectedIcon' | 'childrenIcon' | 'placeholder' | 'autofocus' | 'loading' | 'loadingIcon' | 'closeIcon' | 'back' | 'backIcon' | 'disabled' | 'highlightOnHover' | 'labelKey' | 'descriptionKey' | 'preserveGroupOrder' | 'virtualize' | 'groups'> {
/**
* @defaultValue 'md'
*/
Expand Down Expand Up @@ -99,7 +99,7 @@ const colorMode = useColorMode()
const appConfig = useAppConfig() as DashboardSearch['AppConfig']

const commandPaletteProps = useForwardProps(reactivePick(props, 'size', 'icon', 'trailingIcon', 'selectedIcon', 'childrenIcon', 'placeholder', 'autofocus', 'loading', 'loadingIcon', 'close', 'closeIcon', 'back', 'backIcon', 'disabled', 'highlightOnHover', 'labelKey', 'descriptionKey', 'preserveGroupOrder', 'virtualize', 'searchDelay'))
const modalProps = useForwardProps(reactivePick(props, 'overlay', 'transition', 'content', 'dismissible', 'fullscreen', 'modal', 'portal'))
const modalProps = useForwardProps(reactivePick(props, 'overlay', 'transition', 'content', 'dismissible', 'fullscreen', 'modal', 'portal', 'unmountOnHide'))

const getProxySlots = () => omit(slots, ['content'])

Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ const _props = withDefaults(defineProps<ModalProps>(), {
overlay: true,
transition: true,
modal: true,
dismissible: true
dismissible: true,
unmountOnHide: true
})
const emits = defineEmits<ModalEmits>()
const slots = defineSlots<ModalSlots>()
Expand All @@ -109,7 +110,7 @@ const props = useComponentProps('modal', _props)
const { t } = useLocale()
const appConfig = useAppConfig() as Modal['AppConfig']

const rootProps = useForwardProps(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
const rootProps = useForwardProps(reactivePick(props, 'open', 'defaultOpen', 'modal', 'unmountOnHide'), emits)
const portalProps = usePortal(toRef(() => props.portal))
const contentProps = toRef(() => props.content)
const contentEvents = computed(() => {
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/Slideover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ const _props = withDefaults(defineProps<SlideoverProps>(), {
transition: true,
modal: true,
dismissible: true,
side: 'right'
side: 'right',
unmountOnHide: true
})
const emits = defineEmits<SlideoverEmits>()
const slots = defineSlots<SlideoverSlots>()
Expand All @@ -110,7 +111,7 @@ const props = useComponentProps('slideover', _props)
const { t } = useLocale()
const appConfig = useAppConfig() as Slideover['AppConfig']

const rootProps = useForwardProps(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
const rootProps = useForwardProps(reactivePick(props, 'open', 'defaultOpen', 'modal', 'unmountOnHide'), emits)
const portalProps = usePortal(toRef(() => props.portal))
const contentProps = toRef(() => props.content)
const contentEvents = computed(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/components/content/ContentSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface ContentSearchItem extends Omit<LinkProps, 'custom'>, CommandPal
icon?: IconProps['name']
}

export interface ContentSearchProps<T extends ContentSearchLink = ContentSearchLink> extends Pick<ModalProps, 'title' | 'description' | 'overlay' | 'transition' | 'content' | 'dismissible' | 'fullscreen' | 'modal' | 'portal'>, Pick<CommandPaletteProps<CommandPaletteGroup<ContentSearchItem>, ContentSearchItem>, 'icon' | 'trailingIcon' | 'selectedIcon' | 'childrenIcon' | 'placeholder' | 'autofocus' | 'loading' | 'loadingIcon' | 'closeIcon' | 'back' | 'backIcon' | 'disabled' | 'highlightOnHover' | 'labelKey' | 'descriptionKey' | 'preserveGroupOrder' | 'virtualize' | 'groups'> {
export interface ContentSearchProps<T extends ContentSearchLink = ContentSearchLink> extends Pick<ModalProps, 'title' | 'description' | 'overlay' | 'transition' | 'content' | 'dismissible' | 'fullscreen' | 'modal' | 'portal' | 'unmountOnHide'>, Pick<CommandPaletteProps<CommandPaletteGroup<ContentSearchItem>, ContentSearchItem>, 'icon' | 'trailingIcon' | 'selectedIcon' | 'childrenIcon' | 'placeholder' | 'autofocus' | 'loading' | 'loadingIcon' | 'closeIcon' | 'back' | 'backIcon' | 'disabled' | 'highlightOnHover' | 'labelKey' | 'descriptionKey' | 'preserveGroupOrder' | 'virtualize' | 'groups'> {
/**
* @defaultValue 'md'
*/
Expand Down Expand Up @@ -158,7 +158,7 @@ const colorMode = useColorMode()
const appConfig = useAppConfig() as ContentSearch['AppConfig']

const commandPaletteProps = useForwardProps(reactivePick(props, 'size', 'icon', 'trailingIcon', 'selectedIcon', 'childrenIcon', 'placeholder', 'autofocus', 'loading', 'loadingIcon', 'close', 'closeIcon', 'back', 'backIcon', 'disabled', 'highlightOnHover', 'labelKey', 'descriptionKey', 'preserveGroupOrder', 'virtualize', 'searchDelay'))
const modalProps = useForwardProps(reactivePick(props, 'overlay', 'transition', 'content', 'dismissible', 'fullscreen', 'modal', 'portal'))
const modalProps = useForwardProps(reactivePick(props, 'overlay', 'transition', 'content', 'dismissible', 'fullscreen', 'modal', 'portal', 'unmountOnHide'))

const getProxySlots = () => omit(slots, ['content'])

Expand Down
18 changes: 18 additions & 0 deletions test/components/Modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ describe('Modal', () => {

expect(await axe(wrapper.element)).toHaveNoViolations()
})

it('unmounts content when closed by default', async () => {
const wrapper = await mountSuspended(Modal, {
props: { open: false, portal: false },
slots: { body: () => 'Body content' }
})

expect(wrapper.text()).not.toContain('Body content')
})

it('keeps content mounted when closed with unmountOnHide false', async () => {
const wrapper = await mountSuspended(Modal, {
props: { open: false, portal: false, unmountOnHide: false },
slots: { body: () => 'Body content' }
})

expect(wrapper.text()).toContain('Body content')
})
})
18 changes: 18 additions & 0 deletions test/components/Slideover.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ describe('Slideover', () => {

expect(await axe(wrapper.element)).toHaveNoViolations()
})

it('unmounts content when closed by default', async () => {
const wrapper = await mountSuspended(Slideover, {
props: { open: false, portal: false },
slots: { body: () => 'Body content' }
})

expect(wrapper.text()).not.toContain('Body content')
})

it('keeps content mounted when closed with unmountOnHide false', async () => {
const wrapper = await mountSuspended(Slideover, {
props: { open: false, portal: false, unmountOnHide: false },
slots: { body: () => 'Body content' }
})

expect(wrapper.text()).toContain('Body content')
})
})
Loading