Slideover

对话框GitHub
一个从屏幕任何一侧滑入的对话框。

用法

在 Slideover 的默认插槽中使用Button或任何其他组件。

然后,使用 #content 插槽添加当 Slideover 打开时显示的内容。

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

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>

您还可以使用 #header#body#footer 插槽来自定义 Slideover 的内容。

标题

使用 title prop 设置 Slideover 头部的标题。

<template>
  <USlideover title="Slideover with title">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

描述

使用 description prop 设置 Slideover 头部的描述。

<template>
  <USlideover
    title="Slideover with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

关闭

使用 close prop 自定义或隐藏(使用 false 值)显示在 Slideover 头部中的关闭按钮。

您可以将 Button 组件的任何属性传递过来以进行自定义。

<template>
  <USlideover
    title="Slideover with close button"
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
  >
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>
如果使用了 #content 插槽,则关闭按钮不会显示,因为它是头部的一部分。

关闭图标

使用 close-icon 属性来定制关闭按钮的 Icon。默认为 i-lucide-x

<template>
  <USlideover title="Slideover with close button" close-icon="i-lucide-arrow-right">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>
你可以在 app.config.ts 中的 ui.icons.close 键下全局自定义此图标。
你可以在 vite.config.ts 中的 ui.icons.close 键下全局自定义此图标。

方向

使用 side prop 设置 Slideover 将从屏幕哪一侧滑入。默认为 right

<template>
  <USlideover side="left" title="Slideover with side">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full min-h-48" />
    </template>
  </USlideover>
</template>

嵌入 4.3+

使用 inset prop 将 Slideover 从边缘内嵌。

<template>
  <USlideover side="right" inset title="Slideover with inset">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="min-w-96 min-h-96 size-full" />
    </template>
  </USlideover>
</template>

过渡

使用 transition prop 控制 Slideover 是否有动画。默认为 true

<template>
  <USlideover :transition="false" title="Slideover without transition">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

遮罩层

使用 overlay prop 控制 Slideover 是否有覆盖层。默认为 true

<template>
  <USlideover :overlay="false" title="Slideover without overlay">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

使用 modal prop 控制 Slideover 是否阻止与外部内容的交互。默认为 true

modal设置为false时,叠加层会自动禁用,外部内容变得可交互。
<template>
  <USlideover :modal="false" title="Slideover interactive">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

可关闭

使用 dismissible prop 控制点击外部或按 Esc 键时 Slideover 是否可关闭。默认为 true

当用户尝试关闭时,将发出一个close:prevent事件。
您可以将 modal: falsedismissible: false 结合使用,使 Slideover 的背景可交互而不会关闭它。
<template>
  <USlideover :dismissible="false" title="Slideover non-dismissible">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>
  </USlideover>
</template>

示例

控制打开状态

您可以通过使用default-open prop 或v-model:open指令来控制打开状态。

<script setup lang="ts">
const open = ref(false)

defineShortcuts({
  o: () => open.value = !open.value
})
</script>

<template>
  <USlideover v-model:open="open">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="h-full m-4" />
    </template>
  </USlideover>
</template>
在此示例中,利用 defineShortcuts,您可以通过按 O 键来切换 Slideover。
这允许您将触发器移到 Slideover 外部或完全移除它。

程序化使用

您可以使用 useOverlay 可组合函数以编程方式打开 Slideover。

确保您的应用包装在 App 组件中,该组件使用了OverlayProvider组件的任何属性。

首先,创建一个将以编程方式打开的 slideover 组件

SlideoverExample.vue
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <USlideover
    :close="{ onClick: () => emit('close', false) }"
    :description="`This slideover was opened programmatically ${count} times`"
  >
    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer>
      <div class="flex gap-2">
        <UButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <UButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </USlideover>
</template>
当 slideover 关闭或解除时,我们在此处发出一个 close 事件。您可以通过 close 事件发出任何数据,但是,必须发出该事件才能捕获返回值。

然后,在您的应用程序中使用它

<script setup lang="ts">
import { LazySlideoverExample } from '#components'

const count = ref(0)

const toast = useToast()
const overlay = useOverlay()

const slideover = overlay.create(LazySlideoverExample)

async function open() {
  const instance = slideover.open({
    count: count.value
  })

  const shouldIncrement = await instance.result

  if (shouldIncrement) {
    count.value++

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'slideover-success'
    })

    // Update the count
    slideover.patch({
      count: count.value
    })
    return
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'slideover-dismiss'
  })
}
</script>

<template>
  <UButton label="Open" color="neutral" variant="subtle" @click="open" />
</template>
您可以通过发出 emit('close') 在 slideover 组件内关闭 slideover。

嵌套 slideovers

您可以将 slideovers 相互嵌套。

<script setup lang="ts">
const first = ref(false)
const second = ref(false)
</script>

<template>
  <USlideover v-model:open="first" title="First slideover" :ui="{ footer: 'justify-end' }">
    <UButton color="neutral" variant="subtle" label="Open" />

    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer>
      <UButton label="Close" color="neutral" variant="outline" @click="first = false" />

      <USlideover v-model:open="second" title="Second slideover" :ui="{ footer: 'justify-end' }">
        <UButton label="Open second" color="neutral" />

        <template #body>
          <Placeholder class="h-full" />
        </template>

        <template #footer>
          <UButton label="Close" color="neutral" variant="outline" @click="second = false" />
        </template>
      </USlideover>
    </template>
  </USlideover>
</template>

使用 #footer 插槽在 Slideover 的主体之后添加内容。

<script setup lang="ts">
const open = ref(false)
</script>

<template>
  <USlideover v-model:open="open" title="Slideover with footer" description="This is useful when you want a form in a Slideover." :ui="{ footer: 'justify-end' }">
    <UButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <Placeholder class="h-full" />
    </template>

    <template #footer="{ close }">
      <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
      <UButton label="Submit" color="neutral" />
    </template>
  </USlideover>
</template>

API

属性

属性默认值类型
titlestring
descriptionstring
内容DialogContentProps & Partial<EmitsToProps<DialogContentImplEmits>>

slideover 的内容。

叠加层trueboolean

在 slideover 后面渲染一个覆盖层。

过渡动画trueboolean

打开或关闭 slideover 时进行动画。

side'right'"right" | "top" | "bottom" | "left"

slideover 的侧面。

内嵌falseboolean

是否将 slideover 从边缘内嵌。

portaltruestring | false | true | HTMLElement

在门户中渲染 slideover。

closetrueboolean | Omit<ButtonProps, LinkPropsKeys>

显示一个关闭按钮以关闭 slideover。{ size: 'md', color: 'neutral', variant: 'ghost' }

closeIconappConfig.ui.icons.closeany

关闭按钮中显示的图标。

可关闭的trueboolean

false 时,点击外部或按 Esc 键时 slideover 不会关闭。

openboolean

对话框的受控打开状态。可以绑定为 v-model:open

defaultOpenboolean

对话框初始渲染时的打开状态。当您不需要控制其打开状态时使用。

modaltrueboolean

对话框的模态性。当设置为 true 时,
与外部元素的交互将被禁用,并且只有对话框内容对屏幕阅读器可见。

ui{ overlay?: ClassNameValue; content?: ClassNameValue; header?: ClassNameValue; wrapper?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; close?: ClassNameValue; }

插槽

插槽类型
default{ open: boolean; }
内容{ close: () => void; }
页头{ close: () => void; }
title{}
description{}
actions{}
close{ ui: object; }
主体{ close: () => void; }
页脚{ close: () => void; }

事件

事件类型
after:leave[]
after:enter[]
close:prevent[]
update:open[value: boolean]

主题

app.config.ts
export default defineAppConfig({
  ui: {
    slideover: {
      slots: {
        overlay: 'fixed inset-0 bg-elevated/75',
        content: 'fixed bg-default divide-y divide-default sm:ring ring-default sm:shadow-lg flex flex-col focus:outline-none',
        header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-16',
        wrapper: '',
        body: 'flex-1 overflow-y-auto p-4 sm:p-6',
        footer: 'flex items-center gap-1.5 p-4 sm:px-6',
        title: 'text-highlighted font-semibold',
        description: 'mt-1 text-muted text-sm',
        close: 'absolute top-4 end-4'
      },
      variants: {
        side: {
          top: {
            content: ''
          },
          right: {
            content: 'max-w-md'
          },
          bottom: {
            content: ''
          },
          left: {
            content: 'max-w-md'
          }
        },
        inset: {
          true: {
            content: 'rounded-lg'
          }
        },
        transition: {
          true: {
            overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
          }
        }
      },
      compoundVariants: [
        {
          side: 'top',
          inset: true,
          class: {
            content: 'max-h-[calc(100%-2rem)] inset-x-4 top-4'
          }
        },
        {
          side: 'top',
          inset: false,
          class: {
            content: 'max-h-full inset-x-0 top-0'
          }
        },
        {
          side: 'right',
          inset: true,
          class: {
            content: 'w-[calc(100%-2rem)] inset-y-4 right-4'
          }
        },
        {
          side: 'right',
          inset: false,
          class: {
            content: 'w-full inset-y-0 right-0'
          }
        },
        {
          side: 'bottom',
          inset: true,
          class: {
            content: 'max-h-[calc(100%-2rem)] inset-x-4 bottom-4'
          }
        },
        {
          side: 'bottom',
          inset: false,
          class: {
            content: 'max-h-full inset-x-0 bottom-0'
          }
        },
        {
          side: 'left',
          inset: true,
          class: {
            content: 'w-[calc(100%-2rem)] inset-y-4 left-4'
          }
        },
        {
          side: 'left',
          inset: false,
          class: {
            content: 'w-full inset-y-0 left-0'
          }
        },
        {
          transition: true,
          side: 'top',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-top_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-top_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'right',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-right_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-right_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'bottom',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-bottom_200ms_ease-in-out]'
          }
        },
        {
          transition: true,
          side: 'left',
          class: {
            content: 'data-[state=open]:animate-[slide-in-from-left_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-left_200ms_ease-in-out]'
          }
        }
      ]
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        slideover: {
          slots: {
            overlay: 'fixed inset-0 bg-elevated/75',
            content: 'fixed bg-default divide-y divide-default sm:ring ring-default sm:shadow-lg flex flex-col focus:outline-none',
            header: 'flex items-center gap-1.5 p-4 sm:px-6 min-h-16',
            wrapper: '',
            body: 'flex-1 overflow-y-auto p-4 sm:p-6',
            footer: 'flex items-center gap-1.5 p-4 sm:px-6',
            title: 'text-highlighted font-semibold',
            description: 'mt-1 text-muted text-sm',
            close: 'absolute top-4 end-4'
          },
          variants: {
            side: {
              top: {
                content: ''
              },
              right: {
                content: 'max-w-md'
              },
              bottom: {
                content: ''
              },
              left: {
                content: 'max-w-md'
              }
            },
            inset: {
              true: {
                content: 'rounded-lg'
              }
            },
            transition: {
              true: {
                overlay: 'data-[state=open]:animate-[fade-in_200ms_ease-out] data-[state=closed]:animate-[fade-out_200ms_ease-in]'
              }
            }
          },
          compoundVariants: [
            {
              side: 'top',
              inset: true,
              class: {
                content: 'max-h-[calc(100%-2rem)] inset-x-4 top-4'
              }
            },
            {
              side: 'top',
              inset: false,
              class: {
                content: 'max-h-full inset-x-0 top-0'
              }
            },
            {
              side: 'right',
              inset: true,
              class: {
                content: 'w-[calc(100%-2rem)] inset-y-4 right-4'
              }
            },
            {
              side: 'right',
              inset: false,
              class: {
                content: 'w-full inset-y-0 right-0'
              }
            },
            {
              side: 'bottom',
              inset: true,
              class: {
                content: 'max-h-[calc(100%-2rem)] inset-x-4 bottom-4'
              }
            },
            {
              side: 'bottom',
              inset: false,
              class: {
                content: 'max-h-full inset-x-0 bottom-0'
              }
            },
            {
              side: 'left',
              inset: true,
              class: {
                content: 'w-[calc(100%-2rem)] inset-y-4 left-4'
              }
            },
            {
              side: 'left',
              inset: false,
              class: {
                content: 'w-full inset-y-0 left-0'
              }
            },
            {
              transition: true,
              side: 'top',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-top_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-top_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'right',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-right_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-right_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'bottom',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-bottom_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-bottom_200ms_ease-in-out]'
              }
            },
            {
              transition: true,
              side: 'left',
              class: {
                content: 'data-[state=open]:animate-[slide-in-from-left_200ms_ease-in-out] data-[state=closed]:animate-[slide-out-to-left_200ms_ease-in-out]'
              }
            }
          ]
        }
      }
    })
  ]
})

更新日志

v4.5.0
  • c9704— feat(Theme): 新组件 (#4387)
  • e2c03— fix(Drawer/Modal/Popover/Slideover): 防止在与其他遮罩层交互时发生意外的触摸关闭 (#5695)
v4.3.0
  • 05bd9— feat(Slideover): 添加 inset prop
  • 184ea— chore(components): 通过省略操作按钮的链接属性来减少类型的冗余
v4.2.0
  • dd81d— feat(components): 添加 data-slot 属性 (#5447)
v4.1.0
  • 63c0a— feat(components): 在使用的插槽属性中暴露 ui (#5207)
  • 80994— fix(Drawer/Modal/Slideover): 移除对关闭自动聚焦的阻止 (#5191)
v3.3.3
  • 61b60— feat(Icon): 允许传递组件而非名称 (#4766)
v3.3.1
v3.2.0
  • 81569— feat(Modal/Slideover): 添加 actions slot (#4358)
  • be41a— fix(components): 移除按钮上默认的 md 尺寸 (#4357)
  • 150b3— fix(Modal/Slideover): 不要在 closeAutoFocus 时触发 close:prevent
  • 5835e— feat(Modal/Slideover): 添加 close 方法到插槽 (#4219)
v3.1.3
  • d9e9f— feat(Modal/Slideover): 添加 after:enter 事件 (#4187)
v3.1.0
  • f4864— feat(Modal/Popover/Slideover): 添加 close:prevent 事件 (#3958)
  • 29fa4— feat(App): 添加全局 portal 属性 (#3688)
  • d49e0— feat(module): 定义中性效用类 (neutral utilities) (#3629)
  • 39c86— fix(components): 在 @nuxt/module-builder 升级后重构类型 (#3855)
v3.0.2
  • b9983— fix(components): 优化泛型类型 (#3331)
v3.0.1
  • 5dec0— feat(components): 处理 content 属性中的事件
  • f4c41— fix(Modal/Slideover/Toast): 防止不必要的关闭实例化