# useLayer

## 使用方法

渲染浮层内容的内核：定位、开合、点击外部关闭都在这里。想自己搭 popover、hover card、tooltip 或固定坐标浮层，而现成组件又不够用时，从它开始。

```ts
import { useLayer } from '@astryx-vue/core'
```

`context` 模式跟着锚点元素走；`fixed` 模式放在 `x` / `y` 指定的视口坐标上。开合状态、量出来的坐标、点击外部关闭和浮层 ID 都由它管。它不渲染任何东西：浮层放哪里、长什么样，你自己决定。

```vue
<script setup lang="ts">
import { useLayer, useLayerTeleportTarget } from '@astryx-vue/core'

const layer = useLayer({ mode: 'context', placement: 'below', lightDismiss: true })
// Dialog 打开时浮层挂进 Dialog 里面，其余情况挂到 body。
const teleportTarget = useLayerTeleportTarget()
</script>

<template>
  <button :ref="layer.anchorRef" type="button" @click="layer.show()">
    Show layer
  </button>
  <Teleport :to="teleportTarget">
    <div
      v-if="layer.isMounted"
      v-show="layer.isOpen"
      :id="layer.id"
      :ref="layer.contentRef"
      :style="layer.contentStyle"
    >
      Anchored layer
    </div>
  </Teleport>
</template>
```

### 关闭：一次只关最上面那一层

所有浮层共用一个关闭栈。按 Escape、或者点在浮层外面，都只关最上面那一层：Dialog 里开一个下拉菜单，第一次 Escape 只关菜单，Dialog 还在；再按一次才关 Dialog。点在菜单外面、Dialog 里面，也只关菜单。

点浮层自己的触发器不算「点外面」，那一下交给触发器自己开关，所以不会出现「先被点外面关掉、又被点击重新打开」的闪一下。输入法拼字过程中按 Escape 是取消拼字，不关浮层。

菜单这类要逐层退栈的浮层，可以用 `useDismiss` 的 `onEscape` 单独处理 Escape，点击外面仍然整层关闭。

### 落在哪里

浮层平时挂在 `body`。Dialog 打开时它是一个原生模态层，挂在外面的浮层会被遮罩挡住、也点不到，所以 Dialog 会提供自己内部的挂载点，浮层优先挂进去。`useLayerTeleportTarget()` 会把这目标算好（没有就是 `'body'`），直接给 `Teleport` 用：

```vue
<Teleport :to="teleportTarget">
```

### 文字样式

浮层离开原来的位置之后，仍然会继承触发器所在位置的排版。给浮层根元素加上 `astryx-layer` 类，字号、字重、行高、对齐、字距、换行这些会回到正文的基准，容器内边距系统的变量也会在这里归零。组件自己的 `Popover`、`Tooltip`、`HoverCard`、`DropdownMenu`、`ContextMenu`、`TabMenu`、`Selector` 已经自动带上这些重置。

`contentStyle` 里是量出来的 `top` / `left`（fixed 模式还带 `position: fixed`），所以浮层只需要管自己的背景、圆角和阴影。坐标是按视口算的，请把浮层放进 `Teleport` 渲染，这样也顺带躲开了祖先的裁剪和层叠。

## 建议

| 指引 | 做法 |
| --- | --- |
| Do | 相对某个触发器定位的浮层用 context 模式；要放在指定坐标上的浮层用 fixed 模式。 |
| Do | 常见的浮层形态优先用 `Popover`、`HoverCard`、`Tooltip` 这些更上层的组件。 |
| Do | 用 `Teleport` 渲染浮层，躲开祖先的裁剪和层叠；同时保证它落地的地方拿得到需要的主题变量。 |
| Don't | 除非整套无障碍行为也由你负责，否则不要在浮层里直接手写 ARIA 模式。 |

## 示例

用 `useLayer` 加自定义表面渲染一个底层锚点浮层。

### useLayer — Anchored Layer

用 useLayer 加自定义表面渲染一个底层锚点浮层。

```vue
<script setup lang="ts">
import { useLayer } from '@astryx-vue/core'

const layer = useLayer({ mode: 'context', placement: 'below', alignment: 'center', gap: 8, lightDismiss: true })
const { isOpen, isMounted, contentStyle, show, hide } = layer

function toggle(): void {
  if (isOpen.value)
    hide()
  else
    show()
}
</script>

<template>
  <div class="hook-layer">
    <button :ref="layer.anchorRef" type="button" class="hook-layer__trigger" @click="toggle">
      {{ isOpen ? 'Hide layer' : 'Show layer' }}
    </button>

    <Teleport to="body">
      <div
        v-if="isMounted"
        v-show="isOpen"
        :id="layer.id"
        :ref="layer.contentRef"
        class="hook-layer__surface"
        :style="contentStyle"
      >
        <p class="hook-layer__title">
          Anchored layer
        </p>
        <p class="hook-layer__body">
          Positioned next to its anchor with useLayer. Clicking outside closes it.
        </p>
        <button type="button" class="hook-layer__action" @click="hide">
          Dismiss
        </button>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.hook-layer {
  display: flex;
  justify-content: center;
}

.hook-layer__trigger {
  padding: 8px 16px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: var(--astryx-color-background-surface, #fff);
  color: var(--astryx-color-text-primary, #171717);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.hook-layer__surface {
  position: fixed;
  z-index: 1000;
  box-sizing: border-box;
  width: 16rem;
  padding: 12px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-container, 12px);
  background: var(--astryx-color-background-surface, #fff);
  box-shadow: 0 4px 8px oklch(0 0 0 / 10%);
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
}

.hook-layer__title {
  margin: 0 0 4px;
  font-weight: 600;
}

.hook-layer__body {
  margin: 0 0 10px;
  color: var(--astryx-color-text-secondary, #525252);
  line-height: 1.5;
}

.hook-layer__action {
  padding: 6px 10px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
</style>
```

## 参数

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `mode` | `'context' \| 'fixed'` | 必填 | 定位方式：`context` 跟着锚点元素，`fixed` 用 `x` / `y` 给的坐标。 |
| `anchor` | `Ref` | — | context 模式下外部的锚点元素。不传就把返回的 `anchorRef` 绑到触发器上。 |
| `placement` | `'above' \| 'below' \| 'start' \| 'end' \| MaybeRefOrGetter` | `'below'` | 浮层落在锚点的哪一侧。逻辑方向：`start` / `end` 在 RTL 下自动镜像。空间不够时翻到对面。 |
| `alignment` | `'start' \| 'center' \| 'end' \| MaybeRefOrGetter` | `'center'` | 沿方位轴的对齐方式。 |
| `gap` | `number` | `4` | 浮层与锚点之间的空隙，单位 px。 |
| `x` | `number \| MaybeRefOrGetter` | `0` | fixed 模式的横向视口坐标。 |
| `y` | `number \| MaybeRefOrGetter` | `0` | fixed 模式的纵向视口坐标。 |
| `isDefaultOpen` | `boolean` | `false` | 首次渲染就打开。 |
| `lightDismiss` | `boolean \| MaybeRefOrGetter` | `false` | 点击外部是否关闭浮层。 |
| `lazyMount` | `boolean \| MaybeRefOrGetter` | `false` | 是否等到第一次 `show()` 才渲染内容。渲染与否看 `isMounted`。 |
| `onShow` | `() => void` | — | `show()` 打开浮层后调用。靠 `isDefaultOpen` 首次渲染就打开不算一次显示，不会调用它。 |
| `onHide` | `() => void` | — | `hide()` 关闭浮层后调用。 |

## 返回值

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `isOpen` | `ShallowRef<boolean>` | 浮层当前是否打开。 |
| `anchorRef` | `Ref` | context 模式的锚点元素，用 `:ref` 绑到触发器上；fixed 模式下不用设。 |
| `contentRef` | `Ref` | 浮层容器。定位靠它量尺寸，点击外部也靠它判断。 |
| `contentStyle` | `ComputedRef` | 内联坐标：context 模式是量出来的 `top` / `left`；fixed 模式是 `position: fixed` 加 `x` / `y`。 |
| `resolvedPlacement` | `ComputedRef<'above' \| 'below' \| 'start' \| 'end'>` | 考虑空间翻转之后实际生效的方位。fixed 模式下等于请求的方位。 |
| `isMounted` | `ComputedRef<boolean>` | 内容是否该渲染。`lazyMount` 打开时，第一次 `show()` 之前一直是 false。 |
| `id` | `string` | 浮层的唯一 ID，用于 `aria-controls`、`aria-describedby` 这类关系。 |
| `anchorId` | `string` | 锚点的 CSS 标识，用于高级定位场景。 |
| `show` | `() => void` | 打开浮层。 |
| `hide` | `() => void` | 关闭浮层。 |
