展示类

气泡卡片

点击/鼠标移入元素,弹出气泡式的卡片浮层。

使用场景

Popover 气泡卡片是由用户自主打开的临时性浮层卡片,能够承载一些额外内容和交互行为而不影响原页面。

和 Tooltip 的区别是,它可以承载更复杂的内容,而不仅仅是提示文本。

代码演示

如何引入

import { Popover } from '@fibrous/semi-ui-solid';

注意事项

Popover 需要将触发属性和事件监听器应用到触发元素上,并以触发元素的真实 DOM 元素计算弹出层的定位,因此 children 支持如下类型:

  1. 函数:入参 trigger 即 getTriggerProps,把 trigger() 展开到接收焦点的元素上,该元素可以是真实 DOM 元素(如 span、div、p...),也可以是会把属性透传到根 DOM 元素的组件(如 <Button {...trigger()} />)
  2. 其他内容(文本、组件、多个节点):Popover 会用一个 span 包裹它们作为触发元素
Function
Component
DOM
代码
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-solid';
import { Popover, Space, Empty, Tag } from '@fibrous/semi-ui-solid';

const style = { border: '2px solid var(--semi-color-border)', 'padding-left': '4px', 'padding-right': '4px', 'border-radius': '4px' };

const content = () => (
    <Empty
        title="先进的设计 / 研发协作方式"
        image={<IllustrationSuccess style={{ width: '150px', height: '150px' }} />}
        darkModeImage={<IllustrationSuccessDark style={{ width: '150px', height: '150px' }} />}
        description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
        style={{ width: '400px', margin: '0 auto', display: 'flex', padding: '20px' }}
    />
);

export default function PopoverDemo() {
    return (
        <Space>
            <Popover content={content}>
                {(trigger) => (
                    <span {...trigger()} style={style}>
                        Function
                    </span>
                )}
            </Popover>
            <Popover content={content}>
                <Tag style={style}>Component</Tag>
            </Popover>
            <Popover content={content}>
                <span style={style}>DOM</span>
            </Popover>
        </Space>
    );
}

基本使用

将浮层的触发器 Trigger 作为children,使用 Popover 包裹(如下的例子中触发器为 Tag 元素)。浮层内容通过content传入
注意事项同 Tooltip

悬停此处
代码
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-solid';
import { Popover, Tag, Empty } from '@fibrous/semi-ui-solid';

export default function PopoverDemo() {
    return (
        <Popover
            content={() => (
                <Empty
                    title="先进的设计 / 研发协作方式"
                    image={<IllustrationSuccess style={{ width: '150px', height: '150px' }} />}
                    darkModeImage={<IllustrationSuccessDark style={{ width: '150px', height: '150px' }} />}
                    description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
                    style={{ width: '400px', margin: '0 auto', display: 'flex', padding: '20px' }}
                />
            )}
        >
            <Tag>悬停此处</Tag>
        </Popover>
    );
}

弹出位置

支持通过position设置浮层弹出方向,共支持十二个方向。

TL
Top
TR
LT
Left
LB
RT
Right
RB
BL
Bottom
BR
代码
import { For } from 'solid-js';
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-solid';
import { Popover, Tag, Empty, type TooltipPosition as Position } from '@fibrous/semi-ui-solid';

const tops: [Position, string][] = [
    ['topLeft', 'TL'],
    ['top', 'Top'],
    ['topRight', 'TR'],
];
const lefts: [Position, string][] = [
    ['leftTop', 'LT'],
    ['left', 'Left'],
    ['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
    ['rightTop', 'RT'],
    ['right', 'Right'],
    ['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
    ['bottomLeft', 'BL'],
    ['bottom', 'Bottom'],
    ['bottomRight', 'BR'],
];

const article = () => (
    <Empty
        title="先进的设计 / 研发协作方式"
        image={<IllustrationSuccess style={{ width: '150px', height: '150px' }} />}
        darkModeImage={<IllustrationSuccessDark style={{ width: '150px', height: '150px' }} />}
        description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
        style={{ width: '400px', margin: '0 auto', display: 'flex', padding: '20px' }}
    />
);

export default function PopoverDemo() {
    return (
        <div>
            <div style={{ 'margin-left': '80px', 'white-space': 'nowrap' }}>
                <For each={tops}>
                    {([pos, label]) => (
                        <Popover content={article} position={pos}>
                            <Tag style={{ margin: '8px', padding: '20px' }}>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '80px', float: 'left' }}>
                <For each={lefts}>
                    {([pos, label]) => (
                        <Popover content={article} position={pos}>
                            <Tag style={{ margin: '8px', padding: '20px', width: '60px' }}>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '40px', 'margin-left': '300px' }}>
                <For each={rights}>
                    {([pos, label]) => (
                        <Popover content={article} position={pos}>
                            <Tag style={{ margin: '8px', padding: '20px', width: '60px' }}>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ 'margin-left': '80px', clear: 'both', 'white-space': 'nowrap' }}>
                <For each={bottoms}>
                    {([pos, label]) => (
                        <Popover content={article} position={pos}>
                            <Tag style={{ margin: '8px', padding: '20px', width: '60px' }}>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
        </div>
    );
}

受控显示

设置trigger="custom",此场景下,Popover 的显示与否完全受到参数 visible 的控制。

代码
import { createSignal } from 'solid-js';
import { IllustrationSuccess, IllustrationSuccessDark } from '@fibrous/semi-illustrations-solid';
import { Popover, RadioGroup, Radio, Empty } from '@fibrous/semi-ui-solid';

export default function PopoverDemo() {
    const [visible, setVisible] = createSignal(false);
    return (
        <Popover
            visible={visible()}
            trigger="custom"
            content={() => (
                <Empty
                    title="先进的设计 / 研发协作方式"
                    image={<IllustrationSuccess style={{ width: '150px', height: '150px' }} />}
                    darkModeImage={<IllustrationSuccessDark style={{ width: '150px', height: '150px' }} />}
                    description="使用 Semi D2C 快速还原 Figma 设计稿,一键转代码"
                    style={{ width: '400px', margin: '0 auto', display: 'flex', padding: '20px' }}
                />
            )}
        >
            {(trigger) => (
                <RadioGroup {...trigger()} type="button" onChange={(e) => setVisible(e.target.value)} value={visible()}>
                    <Radio value>受控显示</Radio>
                    <Radio value={false}>受控隐藏</Radio>
                </RadioGroup>
            )}
        </Popover>
    );
}

condition 条件触发

当 condition={false} 时,Popover 不响应 hover/click/focus 等触发行为(trigger="custom" 不受影响);condition 由 true 变为 false 时,已显示的弹出层立即关闭;visible 受控时只调用 onVisibleChange(false),使用方回写 visible 后才关闭。

condition
代码
import { createSignal } from 'solid-js';
import { Popover, Button, Switch, Space, Typography } from '@fibrous/semi-ui-solid';

export default function PopoverDemo() {
    const [enabled, setEnabled] = createSignal(true);
    const content = () => `condition is ${enabled() ? 'true' : 'false'}`;

    return (
        <Space align="center">
            <Typography.Text id="popover-condition-label">condition</Typography.Text>
            <Switch aria-labelledby="popover-condition-label" checked={enabled()} onChange={(v) => setEnabled(v)} />
            <Popover content={content} condition={enabled()}>
                {(trigger) => <Button {...trigger()}>Hover me</Button>}
            </Popover>
            <Popover content={content} trigger="click" condition={enabled()}>
                {(trigger) => <Button {...trigger()}>Click me</Button>}
            </Popover>
        </Space>
    );
}

显示小三角

通过设置showArrow, Popover 同样也支持展示一个小三角。

这种模式下浮层会拥有一个默认的样式,你可以通过传递 style 参数来覆盖掉。

TL
Top
TR
LT
Left
LB
RT
Right
RB
BL
Bottom
BR
代码
import { For } from 'solid-js';
import { Popover, Tag, type TooltipPosition as Position } from '@fibrous/semi-ui-solid';

const tops: [Position, string][] = [
    ['topLeft', 'TL'],
    ['top', 'Top'],
    ['topRight', 'TR'],
];
const lefts: [Position, string][] = [
    ['leftTop', 'LT'],
    ['left', 'Left'],
    ['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
    ['rightTop', 'RT'],
    ['right', 'Right'],
    ['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
    ['bottomLeft', 'BL'],
    ['bottom', 'Bottom'],
    ['bottomRight', 'BR'],
];

const article = () => (
    <article>
        Hi ByteDancer, this is a popover.
        <br /> We have 2 lines.
    </article>
);

export default function PopoverDemo() {
    return (
        <div style={{ 'padding-left': '40px' }} class="tag-margin-right">
            <div style={{ 'margin-left': '40px', 'white-space': 'nowrap' }}>
                <For each={tops}>
                    {([pos, label]) => (
                        <Popover showArrow content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '40px', float: 'left' }}>
                <For each={lefts}>
                    {([pos, label]) => (
                        <Popover showArrow content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '40px', 'margin-left': '180px' }}>
                <For each={rights}>
                    {([pos, label]) => (
                        <Popover showArrow content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ 'margin-left': '40px', clear: 'both', 'white-space': 'nowrap' }}>
                <For each={bottoms}>
                    {([pos, label]) => (
                        <Popover showArrow content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
        </div>
    );
}

指向元素中心

在显示小三角的条件(showArrow=true)下,可以传入 arrowPointAtCenter=true 使得小三角始终指向元素中心位置。

TL
Top
TR
LT
Left
LB
RT
Right
RB
BL
Bottom
BR
代码
import { For } from 'solid-js';
import { Popover, Tag, type TooltipPosition as Position } from '@fibrous/semi-ui-solid';

const tops: [Position, string][] = [
    ['topLeft', 'TL'],
    ['top', 'Top'],
    ['topRight', 'TR'],
];
const lefts: [Position, string][] = [
    ['leftTop', 'LT'],
    ['left', 'Left'],
    ['leftBottom', 'LB'],
];
const rights: [Position, string][] = [
    ['rightTop', 'RT'],
    ['right', 'Right'],
    ['rightBottom', 'RB'],
];
const bottoms: [Position, string][] = [
    ['bottomLeft', 'BL'],
    ['bottom', 'Bottom'],
    ['bottomRight', 'BR'],
];

const article = () => (
    <article>
        Hi ByteDancer, this is a popover.
        <br /> We have 2 lines.
    </article>
);

export default function PopoverDemo() {
    return (
        <div style={{ 'padding-left': '40px' }} class="tag-margin-right">
            <div style={{ 'margin-left': '40px', 'white-space': 'nowrap' }}>
                <For each={tops}>
                    {([pos, label]) => (
                        <Popover showArrow arrowPointAtCenter content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '40px', float: 'left' }}>
                <For each={lefts}>
                    {([pos, label]) => (
                        <Popover showArrow arrowPointAtCenter content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ width: '40px', 'margin-left': '180px' }}>
                <For each={rights}>
                    {([pos, label]) => (
                        <Popover showArrow arrowPointAtCenter content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
            <div style={{ 'margin-left': '40px', clear: 'both', 'white-space': 'nowrap' }}>
                <For each={bottoms}>
                    {([pos, label]) => (
                        <Popover showArrow arrowPointAtCenter content={article} position={pos}>
                            <Tag>{label}</Tag>
                        </Popover>
                    )}
                </For>
            </div>
        </div>
    );
}

设置浮层背景色

如果你需要定制浮层的背景色或边框颜色,请务必单独声明 style 中的 backgroundColor 和 borderColor 属性,这样能够使得“小三角”也能应用相同的背景色和边框颜色。

Colorful Popover
代码
import { Popover, Tag } from '@fibrous/semi-ui-solid';

export default function PopoverDemo() {
    return (
        <div>
            <Popover
                aria-label="Colorful Popover"
                content={<article style={{ padding: '4px' }}>Hi, Semi UI Popover.</article>}
                trigger="custom"
                visible
                position="right"
                showArrow
                style={{
                    'background-color': 'var(--semi-blue-6)',
                    'border-color': 'var(--semi-blue-6)',
                    color: 'var(--semi-color-white)',
                    'border-width': '1px',
                    'border-style': 'solid',
                }}
            >
                <Tag style={{ 'background-color': 'var(--semi-blue-6)', color: 'var(--semi-color-white)' }}>Colorful Popover</Tag>
            </Popover>
        </div>
    );
}

初始化弹出层焦点位置

Popover content 支持传入函数,它的入参是一个对象,将 initialFocusRef 作为 ref 绑定在可聚焦 DOM 或组件上,打开面板时会自动聚焦在该位置。

代码
import { Button, Input, Popover, Space, type TooltipRenderContentProps } from '@fibrous/semi-ui-solid';

export default function PopoverDemo() {
    const renderContent = (props: TooltipRenderContentProps) => (
        <div style={{ padding: '12px' }}>
            <Space>
                <Button>first focusable element</Button>
                <Input ref={props.initialFocusRef} placeholder="focus here" />
            </Space>
        </div>
    );

    return (
        <Popover content={renderContent} trigger="click">
            {(trigger) => <Button {...trigger()}>click me</Button>}
        </Popover>
    );
}

搭配 Tooltip 或 Popconfirm 使用

请参考搭配使用

API 参考

Props

属性说明类型默认值
afterClose弹出层关闭且离场动画结束后触发。Type() => void-
afterOpen弹出层打开且进场动画结束后触发,进场结束前被关闭的打开不触发。TooltipProps['afterOpen']-
aria-label弹出层 role 为 dialog 时的名称。string-
aria-labelledby弹出层 role 为 dialog 时,指向名称元素的 id。string-
arrowPointAtCenter小三角是否指向触发元素中心,需要同时开启 showArrow。未设置时为 true。boolean-
arrowStyle小三角的 borderColor、backgroundColor 与 borderOpacity(驼峰键名的对象,不是 Solid 的 style 对象);缺省跟随对象形式 style 中的 background-color 与 border-color。ArrowStyle-
autoAdjustOverflow弹出层被遮挡时是否自动调整方向。booleantrue
children触发元素。传入以 getTriggerProps 为参数的函数时,把 trigger() 展开到接收焦点的元素上;其他内容会被包在 Tooltip 的 span 里作为触发元素。TooltipProps['children']-
class弹出层的类名。string-
clickToHide点击弹出层及其内部任一元素时是否关闭弹出层。TooltipProps['clickToHide']-
closeOnEsc弹出层打开期间按 Esc 是否关闭弹出层;trigger 为 custom 时只调用 onEscKeyDown,visible 受控时只调用 onVisibleChange(false),使用方回写后才关闭。TooltipProps['closeOnEsc']true
condition是否允许触发显示。仅显式设为 false 时,hover、click、focus 等触发行为不生效,trigger 为 custom 时仍按 visible 显示;由 true 变为 false 时立即关闭已显示的弹出层,visible 受控时只调用 onVisibleChange(false),使用方回写后才关闭。boolean-
content弹出层内容。可传 JSX,也可传函数;函数的入参是 { initialFocusRef },把 initialFocusRef 作为 ref 绑定到可聚焦元素上,打开时会自动聚焦在该位置。TooltipProps['content']-
contentClassName弹出层内容卡片的类名。ClassValue-
disableArrowKeyDown是否禁用在触发元素上按方向键把焦点移入弹出层。TooltipProps['disableArrowKeyDown']-
disableFocusListenertrigger 为 hover 时,是否不响应键盘聚焦打开弹出层。boolean-
guardFocus焦点位于弹出层内时,Tab 是否在弹出层内循环。TooltipProps['guardFocus']true
keepDOM关闭时是否保留弹出层内容而不销毁。boolean-
motion是否展示弹出层动画。booleantrue
mouseEnterDelay指针移入后延迟显示的毫秒数,仅 trigger 为 hover 或 focus 时生效。未设置时为 50。number-
mouseLeaveDelay指针移出后延迟隐藏的毫秒数,仅 trigger 为 hover 或 focus 时生效,不小于 mouseEnterDelay。未设置时为 50。number-
onClickOutSide弹出层处于展示状态时,点击触发元素和弹出层之外区域的回调,参数是原生 MouseEvent;仅 trigger 为 custom 或 click 时有效。TooltipProps['onClickOutSide']-
onEscKeyDown弹出层打开期间按 Esc 时调用,参数是原生 KeyboardEvent。TooltipProps['onEscKeyDown']-
onOpen弹出层显示、进场动画开始时调用,每次打开一次,早于 afterOpen。TooltipProps['onOpen']-
onVisibleChange弹出层显示或隐藏时的回调;visible 受控时只报告用户交互发起的请求,使用方自己改变 visible 不触发。Type(visible: boolean) => void-
position弹出层相对触发元素的位置,共十二个方向。Position"bottom"
prefixCls弹出层类名前缀,设置后弹出层不再带 Popover 的样式。string"semi-popover"
ref接收 Popover 实例的方法,见下方 Methods。TypeRef<PopoverHandle>-
returnFocusOnClose按 Esc 关闭弹出层后焦点是否回到触发元素。TooltipProps['returnFocusOnClose']true
role弹出层的 role。不传时 trigger 为 click、contextMenu、custom 是 dialog,其余是 tooltip;弹出层只是另一个控件的附属(如 combobox 的下拉)时传 presentation。string-
showArrow是否显示小三角。booleanfalse
spacing弹出层与触发元素的距离,单位为像素;对象形式分别设置水平与垂直方向。不传时无小三角为 4,有小三角为 10。number | { x: number; y: number }-
stopPropagation是否阻止弹出层上的点击事件冒泡。boolean | string-
style弹出层的内联样式,可传 style 对象或字符串。小三角只从对象形式中读取 background-color 与 border-color。JSX.CSSProperties | string-
trigger触发显示的时机,可选 hover、focus、click、custom 或 contextMenu。custom 时显示与否完全由 visible 决定。Trigger"hover"
visible是否显示弹出层(受控)。传入后触发器、点击外部与 Esc 只通过 onVisibleChange 请求打开或关闭,使用方回写后才生效;不需要触发器事件时配合 trigger 为 custom 使用。boolean-

Methods

方法说明类型
focusTrigger让触发元素获得焦点,仅在 returnFocusOnClose 为 true 且 trigger 不是 custom 时生效。Type() => void
rePosition按触发器当前位置立即重新计算弹出层位置。弹出层已随滚动、窗口尺寸变化、触发器移动以及触发器或弹出层的尺寸变化自动重新定位,一般无需调用;跟随触发器移动会晚一帧,需要同一帧内对齐时调用。Type() => void

class、style 落在弹出层上,其他未声明的属性会被丢弃(Popover 没有根 DOM 元素);触发属性只加在触发元素上。ref 收到的是实例方法,不是 DOM 元素。

Accessibility

ARIA

  • 关于 role
    • 当 Popover 的 trigger 为 click、contextMenu、custom 时,Popover 的 content 具有 dialog role
    • 当 trigger 为 hover、focus 时,Popover 的 content 具有 tooltip role
    • role 属性可覆盖默认值。dialog 需要名称:传入 aria-label,或用 aria-labelledby 指向 content 中的标题;未命名的 dialog 在开发环境会输出警告
  • Popover 的 content
    • content 的 wrapper 会被自动添加 id 属性
  • Popover 的 children(tooltip 只会被添加 aria-describedby;presentation 等其他 role 不添加任何属性)
    • 会被自动添加 aria-expanded 属性,当 Popover 可见时,属性值为 true,不可见时为 false
    • 会被自动添加 aria-haspopup 属性,为 dialog
    • 会被自动添加 aria-controls 属性,为 content 的 wrapper 的 id

键盘和焦点

  • Popover 触发方式设置为 hover 时:鼠标悬浮或聚焦时打开 Popover
  • Popover 触发方式设置为 click 时:点击触发器或聚焦时并使用 Enter 键打开 Popover
  • Popover 激活后,按下方向键 ⬇️ 将焦点移动到 Popover 上,此时焦点默认处于 Popover 中第一个可交互元素上,用户也可自定义焦点位置(若 Popover 内无可交互元素则表现为无响应)
  • 焦点处于 Popover 内时使用 Tab 键,焦点会在 Popover 内循环,使用 Shift + Tab 会反方向移动焦点
  • 键盘用户能够通过按 Esc 关闭 Popover,关闭后焦点返回到触发器上(仅当 trigger 为 click 时)

设计变量

下表是组件内部的 Sass 变量,不能覆写。定制样式请覆写 --semi-* CSS 变量(见 Design Token 页),或写不分层的 CSS 规则。
Sass 变量值用法
$animation_duration-popover-out100mspopover-显示及收起动画-动画持续时间
$animation_function-popover-outcubic-bezier(0.215, 0.61, 0.355, 1)popover-显示及收起动画-动画插值函数
$horizontal-rate-popover0.24水平方向矫正因子
$color-popover_arrow-icon-defaultunset箭头图标
$color-popover-bg-defaultvar(--semi-color-bg-3)默认背景色
$color-popover-border-defaultvar(--semi-color-border)默认描边颜色
$color-popover-arrow-bordervar(--semi-color-border)箭头描边颜色
$color-popover-arrow-bgvar(--semi-color-bg-3)箭头默认背景色
$width-popover_arrow24px水平箭头宽度
$height-popover_arrow8px水平箭头高度
$height-popover_arrow_vertical24px垂直箭头高度
$width-popover_arrow_vertical8px垂直箭头宽度
$width-popover_title-border1px
$spacing-popover_title-padding8px气泡卡片标题内边距
$spacing-popover_withArrow-padding12px带箭头的气泡卡片标题内边距
$spacing-popover_arrow_adjusted_offset-y6px垂直方向上的校正偏移量,主要修正三角形有棱角的问题
$spacing-popover_arrow_adjusted_offset-xmath.round($horizontal-rate-popover * $width-popover_arrow)水平方向上的校正偏移量,主要修正三角形有棱角的问题
$radius-popovervar(--semi-border-radius-medium)气泡卡片的圆角大小
$filter-popover-bgnone

FAQ

  • 为什么 Popover 浮层卡片的位置和浮层的触发器的相对位置不符合预期?
    Popover 底层依赖了 Tooltip,Tooltip 为了计算定位,需要获取到 children 的真实 DOM 元素,因此 Popover 类型目前支持如下类型的 children:

    1. 函数:把 trigger() 展开到真实 DOM 元素,或展开到会把属性透传到根 DOM 元素的组件上
    2. 其他内容(文本、组件、多个节点),由 Popover 用一个 span 包裹

    若触发元素的真实 DOM 节点宽高并非是你的触发内容的全部,则位置可能有出入。例如设置了 prefix、suffix 的 Input,Popover位置仍是相对于不包含前缀部分的 input 框进行定位,此时只要在 Input 外层再套一个 div 就能解决问题。

  • 为什么 Popover 浮层卡片在靠近屏幕边界宽度不够时,丢失宽度意外换行?
    在 chromium 104 后 对于屏幕边界文本宽度不够时的换行渲染策略发生变化,详细原因可查看 issue #1022,semi侧已经在v2.17.0版本修复了这个问题。