代码演示
如何引入
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
import { ResizeItem, ResizeHandler, ResizeGroup } from '@fibrae/semi-ui-svelte';
</script>
单个组件 基本使用
通过defaultSize设置初始大小,可以通过onResizeStart、onChange、onResizeEnd设置拖拽的回调
onResizeStart、onChange、onResizeEnd 都是回调 prop,onResizeStart 返回 false 可取消本次伸缩;style 只接受字符串。
interface Size {
width?: string | number;
height?: string | number;
}
代码
<script lang="ts">
import { Resizable, Toast } from '@fibrae/semi-ui-svelte';
let text = $state('Drag edge to resize');
const opts_1 = {
content: 'resize start',
duration: 1,
stack: true,
};
const opts_2 = {
content: 'resize end',
duration: 1,
stack: true,
};
const onResizeStart = () => {
Toast.info(opts_1);
};
const onResizeEnd = () => {
Toast.info(opts_2);
text = 'Drag edge to resize';
};
</script>
<div style="width: 500px">
<Resizable defaultSize={{ width: '60%', height: 300 }} onChange={() => (text = 'resizing')} {onResizeEnd} {onResizeStart} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">{text}</div>
</Resizable>
</div>控制伸缩方向
通过设置enable的值开启/关闭特定伸缩方向,默认值均为true;未写出的方向视为开启,要只开启部分方向,需把其余方向显式设为 false。enable 传 false 时不渲染任何手柄。
interface Enable {
left?: boolean;
right?: boolean;
top?: boolean;
bottom?: boolean;
topLeft?: boolean;
topRight?: boolean;
bottomLeft?: boolean;
bottomRight?: boolean;
}
disable
代码
<script lang="ts">
import { Resizable, Switch, TypographyTitle } from '@fibrae/semi-ui-svelte';
let enabled = $state(false);
</script>
<div style="width: 500px; height: 60%">
<div style="display: flex; align-items: center; margin: 8px">
<Switch aria-labelledby="resizable-enable-state" bind:checked={enabled} />
<TypographyTitle heading={6} id="resizable-enable-state" style="margin: 8px">
{enabled ? 'able' : 'disable'}
</TypographyTitle>
</div>
<Resizable defaultSize={{ width: 200, height: 200 }} enable={{ left: enabled }} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">{'enable.left:' + enabled}</div>
</Resizable>
</div>设置变化比例
通过ratio设置拖动和实际变化的比例
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%">
<Resizable defaultSize={{ width: 200, height: 200 }} ratio={2} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">ratio=2</div>
</Resizable>
</div>锁定横纵比
通过lockAspectRatio设置锁定横纵比,可以为boolean或number,为number时表示横纵比为number,为true时锁定初始横纵比
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%">
<Resizable defaultSize={{ width: 400, height: 300 }} lockAspectRatio style="background-color: var(--semi-grey-1); margin-bottom: 10px">
<div style="margin-left: 20%">lock</div>
</Resizable>
<Resizable defaultSize={{ width: 200, height: (200 * 9) / 16 }} lockAspectRatio={16 / 9} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">16 / 9</div>
</Resizable>
</div>设置最大,最小宽高
可通过 maxHeight,maxWidth,minHeight,minWidth 设置最大,最小宽高
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%">
<Resizable defaultSize={{ width: 100, height: 100 }} maxHeight={300} maxWidth={200} minHeight={50} minWidth={50} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">width在50到200之间,height在50到300之间</div>
</Resizable>
</div>受控宽高
可通过 size 控制元素的宽高
size 是单向 prop,不能 bind:size:拖动时需在 onChange 中把新尺寸写回传给 size 的状态,尺寸才会随拖拽变化;未写回时,尺寸保持为 size 的值。
代码
<script lang="ts">
import { Button, Resizable, type ResizableProps } from '@fibrae/semi-ui-svelte';
let size = $state<NonNullable<ResizableProps['size']>>({ width: 200, height: 100 });
const grow = () => {
size = { width: Number(size.width) + 10, height: Number(size.height) + 10 };
};
</script>
<div style="width: 500px; height: 60%">
<Button onclick={grow}>set += 10</Button>
<Resizable onChange={(next) => (size = next)} {size} style="background-color: var(--semi-grey-1); margin-top: 10px">
<div style="margin-left: 20%">受控</div>
</Resizable>
</div>设置缩放值
通过设置 scale,整体缩放元素
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%; transform: scale(0.5); transform-origin: 0 0">
<Resizable defaultSize={{ width: '60%', height: '60%' }} scale={0.5} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">scale 0.5</div>
</Resizable>
</div>根据元素限制元素宽高
通过 boundElement 设置用于限制宽高的元素,支持 'parent'、'window' 或一个 HTMLElement
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 300px; height: 300px; border: var(--semi-color-border) 1px solid">
<Resizable boundElement="parent" defaultSize={{ width: '60%', height: 200 }} style="margin-left: 20%; background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">bound:parent</div>
</Resizable>
</div>自定义边角 handler 样式
可通过 handle snippet 设置各方向的拖动元素节点:snippet 对每个启用的方向渲染一次,参数 { direction } 为该手柄的方向,按方向返回内容;渲染空内容的方向仍保留手柄本身。可通过 handleStyle,handleClass 设置不同方向上的样式,样式的值是字符串。
type HandleStyle = {
left?: string;
right?: string;
top?: string;
bottom?: string;
topLeft?: string;
topRight?: string;
bottomLeft?: string;
bottomRight?: string;
};
type HandleClass = {
left?: string;
right?: string;
top?: string;
bottom?: string;
topLeft?: string;
topRight?: string;
bottomLeft?: string;
bottomRight?: string;
};
代码
<script lang="ts">
import { IconTransfer } from '@fibrae/semi-icons-svelte/lab';
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%">
<Resizable defaultSize={{ width: '60%', height: 300 }} style="margin-left: 20%; background-color: var(--semi-grey-1); border: var(--semi-color-border) 1px solid">
<div style="margin-left: 20%">right</div>
{#snippet handle({ direction })}
{#if direction === 'right'}
<div style="height: 100%; display: flex; align-items: center; width: fit-content">
<IconTransfer />
</div>
{/if}
{/snippet}
</Resizable>
</div>允许阶段性调整宽高
可通过 grid ,snap 属性允许逐渐调整宽高。 grid 属性用于指定调整大小应对齐的增量。默认为 [1, 1]。 snap 属性用于指定调整大小时应对齐的绝对像素值。 x 和 y 都是可选的,允许仅包含要定义的轴。默认为空。以上两个参数可结合 snapGap 使用,该参数用于指定移动到下一个目标所需的最小间隙。默认为 0,这意味着始终使用 grid/snap 设定的目标。
interface Snap {
x?: number[];
y?: number[];
}
代码
<script lang="ts">
import { Resizable } from '@fibrae/semi-ui-svelte';
</script>
<div style="width: 500px; height: 60%">
<Resizable defaultSize={{ width: '60%', height: 300 }} grid={[100, 100]} snapGap={20} style="margin-left: 20%; background-color: var(--semi-grey-1); border: var(--semi-color-border) 1px solid">
<div style="margin-left: 20%">snap</div>
</Resizable>
</div>组合组件 基本使用
通过direction设置伸缩方向,可选值为horizontal和vertical,缺省为horizontal。支持onResizeStart、onChange、onResizeEnd回调,支持min、max设置最大最小宽高
ResizeItem 的 onResizeStart、onChange、onResizeEnd 同样是回调 prop。
代码
<script lang="ts">
import { ResizeGroup, ResizeHandler, ResizeItem } from '@fibrae/semi-ui-svelte';
let text = $state('Drag to resize');
const itemStyle = 'background-color: var(--semi-grey-1); border: var(--semi-color-border) 1px solid';
</script>
<div style="width: 1000px; height: 100px">
<ResizeGroup direction="horizontal">
<ResizeItem defaultSize="400px" min="10%" onChange={() => (text = 'resizing')} onResizeEnd={() => (text = 'Drag to resize')} style={itemStyle}>
<div style="margin-left: 20%">{text + ' min:10%'}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="20%" max="30%" min="10%" onChange={() => (text = 'resizing')} style={itemStyle}>
<div style="margin-left: 20%">{text + ' min:10% max:30%'}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="0.5" onChange={() => (text = 'resizing')} style={itemStyle}>
<div style="margin-left: 20%">{text}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize={1} onChange={() => (text = 'resizing')} style={itemStyle}>
<div style="margin-left: 20%">{text}</div>
</ResizeItem>
</ResizeGroup>
</div>嵌套使用
通过direction设置伸缩方向,可选值为horizontal和vertical
代码
<script lang="ts">
import { ResizeGroup, ResizeHandler, ResizeItem, Toast } from '@fibrae/semi-ui-svelte';
let text = $state('Drag to resize');
const opts_1 = {
content: 'resize start',
duration: 1,
stack: true,
};
const opts = {
content: 'resize end',
duration: 1,
stack: true,
};
const itemStyle = 'background-color: var(--semi-grey-1); border: var(--semi-color-border) 1px solid';
const onResizeStart = () => {
Toast.info(opts_1);
};
const onResizeEnd = () => {
Toast.info(opts);
text = 'Drag to resize';
};
</script>
<div style="width: 1000px; height: 600px">
<ResizeGroup direction="vertical">
<ResizeItem defaultSize="20%" onChange={() => (text = 'resizing')} {onResizeEnd} {onResizeStart} style="background-color: var(--semi-grey-1)">
<div style="margin-left: 20%">header</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="80%" onChange={() => (text = 'resizing')}>
<ResizeGroup direction="horizontal">
<ResizeItem defaultSize="25%" onChange={() => (text = 'resizing')} {onResizeEnd} {onResizeStart} style={itemStyle}>
<div style="margin-left: 20%">tab</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="75%" onChange={() => (text = 'resizing')} style={itemStyle}>
<div style="margin-left: 20%">{text}</div>
</ResizeItem>
</ResizeGroup>
</ResizeItem>
</ResizeGroup>
</div>代码
<script lang="ts">
import { ResizeGroup, ResizeHandler, ResizeItem } from '@fibrae/semi-ui-svelte';
let text = $state('Drag to resize');
const itemStyle = 'background-color: var(--semi-grey-1); border: var(--semi-color-border) 1px solid';
const borderStyle = 'border: var(--semi-color-border) 1px solid';
</script>
<div style="width: 1000px; height: 600px">
<ResizeGroup direction="vertical">
<ResizeItem defaultSize="80%">
<ResizeGroup direction="horizontal">
<ResizeItem defaultSize="25%" max="30%" min="10%" style={itemStyle}>
<div style="margin-left: 20%">{text + ' min:10% max:30%'}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="50%" style={borderStyle}>
<div style="height: 100%">
<ResizeGroup direction="vertical">
<ResizeItem defaultSize="33%" min="10%" onChange={() => (text = 'resizing')} onResizeEnd={() => (text = 'Drag to resize')} style={itemStyle}>
<div style="margin-left: 20%">{text + ' min:10%'}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="33%" max="40%" min="10%" style={itemStyle}>
<div style="margin-left: 20%">{text + ' min:10% max:40%'}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem style={itemStyle}>
<div style="margin-left: 20%">{text}</div>
</ResizeItem>
</ResizeGroup>
</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="1" max="30%" style={itemStyle}>
<div style="margin-left: 20%">{text + ' max:30%'}</div>
</ResizeItem>
</ResizeGroup>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="20%" onChange={() => (text = 'resizing')}>
<ResizeGroup direction="horizontal">
<ResizeItem defaultSize="50%" style={itemStyle}>
<div style="margin-left: 20%">tab</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize="50%" style={itemStyle}>
<div style="margin-left: 20%">content</div>
</ResizeItem>
</ResizeGroup>
</ResizeItem>
</ResizeGroup>
</div>动态方向
代码
<script lang="ts">
import { Button, ResizeGroup, ResizeHandler, ResizeItem } from '@fibrae/semi-ui-svelte';
let text = $state('drag to resize');
let direction = $state<'horizontal' | 'vertical'>('horizontal');
const itemStyle = 'background-color: var(--semi-grey-1)';
const contentStyle = 'margin-left: 20%; padding: 5px';
const changeDirection = () => {
direction = direction === 'horizontal' ? 'vertical' : 'horizontal';
};
</script>
<div style="width: 400px; height: 300px">
<Button onclick={changeDirection}>{direction}</Button>
<ResizeGroup {direction}>
<ResizeItem defaultSize={5} onChange={() => (text = 'resizing')} onResizeEnd={() => (text = 'drag to resize')}>
<ResizeGroup direction="horizontal">
<ResizeItem onChange={() => (text = 'resizing')} onResizeEnd={() => (text = 'drag to resize')} style={itemStyle}>
<div style={contentStyle}>{text}</div>
</ResizeItem>
<ResizeHandler />
<ResizeItem onChange={() => (text = 'resizing')} style={itemStyle}>
<div style={contentStyle}>{text}</div>
</ResizeItem>
</ResizeGroup>
</ResizeItem>
<ResizeHandler />
<ResizeItem defaultSize={1.3} onChange={() => (text = 'resizing')} style={itemStyle}>
<div style={contentStyle}>{text}</div>
</ResizeItem>
</ResizeGroup>
</div>API 参考
Resizable
单个伸缩框组件。
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| boundElement | 限制宽高的元素:'parent'、'window' 或一个 HTMLElement。 | 'parent' | 'window' | HTMLElement | - |
| boundsByDirection | 是否按拖动方向计算边界:为 true 时,从左侧或上侧手柄拖动,最大尺寸按元素固定的那一侧到限制元素对侧边缘的距离计算。 | boolean | - |
| children | 伸缩框的内容。 | Snippet | - |
| defaultSize | 初始宽高,支持数字和字符串(px/vw/vh/%);也是 size 未设置的轴的回退值。 | Size | - |
| enable | 可伸缩的方向,未设置为 false 的方向均可拖动;传入 false 则不渲染任何手柄。 | Enable | false | Type{ top: true, right: true, bottom: true, left: true, topRight: true, bottomRight: true, bottomLeft: true, topLeft: true } |
| grid | 调整大小时对齐的增量。 | [number, number] | [1, 1] |
| handle | 各启用方向手柄内容的 snippet,每个启用的方向渲染一次,参数 { direction } 为该手柄的方向。 | TypeSnippet<[ResizableHandleSnippetProps]> | - |
| handleClass | 各方向手柄的类名。 | HandleClassName | - |
| handleStyle | 各方向手柄的样式,值为样式字符串。 | HandleStyle | - |
| handleWrapperClass | 包裹所有手柄的容器类名。 | string | - |
| handleWrapperStyle | 包裹所有手柄的容器样式字符串。 | string | - |
| lockAspectRatio | 锁定横纵比:true 按初始宽高锁定,数字为宽高比。 | boolean | number | false |
| lockAspectRatioExtraHeight | 锁定横纵比时,不参与比例计算的额外高度(如内边距),单位为像素。 | number | 0 |
| lockAspectRatioExtraWidth | 锁定横纵比时,不参与比例计算的额外宽度(如内边距),单位为像素。 | number | 0 |
| maxHeight | 最大高度。 | string | number | - |
| maxWidth | 最大宽度。 | string | number | - |
| minHeight | 最小高度。 | string | number | - |
| minWidth | 最小宽度。 | string | number | - |
| onChange | 拖动或方向键改变尺寸时调用,参数为新尺寸、事件与方向。 | ResizeCallback | - |
| onResizeEnd | 结束伸缩时调用,参数为最终尺寸、事件与方向。 | ResizeCallback | - |
| onResizeStart | 开始伸缩时调用,返回 false 取消本次伸缩。 | ResizeStartCallback | - |
| ratio | 拖动距离与实际尺寸变化的比例,可传 [宽, 高]。方向键每次固定改变 10px,不受 ratio 与 scale 影响。 | number | [number, number] | 1 |
| scale | 元素被外部缩放的比例,用于校正指针位移。 | number | 1 |
| size | 受控尺寸,按轴生效:未设置的轴仍跟随拖拽。单向 prop,不能 bind:size,在 onChange 里写回新尺寸。 | Size | - |
| snap | 调整大小时对齐的绝对像素值,x 与 y 均可省略。 | { x?: number[]; y?: number[] } | - |
| snapGap | 移动到下一个对齐目标所需的最小间隙。 | number | 0 |
class、style(字符串)与原生属性落在根元素上;根元素的 style 先于组件计算出的尺寸,所以传入的 width、height 不会让拖拽失效。
ResizeGroup
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | ResizeItem 与 ResizeHandler,子项挂载后自行向组注册。 | Snippet | - |
| direction | 组内的伸缩方向,horizontal 为左右排列,vertical 为上下排列。 | 'horizontal' | 'vertical' | "horizontal" |
class、style(字符串)与原生属性落在根元素上;flex-direction 始终由 direction 决定。
ResizeHandler
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 替换默认的握把图标。 | Snippet | - |
class、style(字符串)与原生属性落在手柄元素上;传入的 aria-label 优先于语言包名称。
ResizeItem
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 子项的内容。 | Snippet | - |
| defaultSize | 初始尺寸。字符串支持 %、px、vw、vh;纯数字或数字字符串表示按比例分配剩余空间。 | string | number | - |
| max | 最大尺寸(%、px、vw 或 vh)。 | string | - |
| min | 最小尺寸(%、px、vw 或 vh)。 | string | - |
| onChange | 拖动或方向键改变尺寸时调用,参数为新尺寸、事件与方向。 | ResizeCallback | - |
| onResizeEnd | 结束伸缩时调用,参数为最终尺寸、事件与方向。 | ResizeCallback | - |
| onResizeStart | 开始伸缩时调用,手柄任一侧的项返回 false 即取消本次伸缩。 | ResizeStartCallback | - |
class、style 与原生属性落在根元素上。ResizeGroup 把尺寸直接写在元素的 style.width 与 style.height 上,Svelte 的 style 只接受字符串,字符串变化时会整体重新写入,把尺寸丢掉,因此 style 请写成不变的值。
Accessibility
- 单个伸缩框每个轴上只有一个手柄是可聚焦的
separator:右侧与下侧手柄始终是;对应的右侧或下侧被enable关闭时,该轴由左侧或上侧手柄接替。名称取自语言包,分别调整宽度与高度;聚焦后按方向键,每次固定改变 10px,不受ratio、scale影响;同时按住 Alt、Ctrl 或 Meta 时不响应。鼠标右键、中键按下手柄不开始调整。 ResizeHandler是可聚焦的separator,名称同样取自语言包,传入aria-label可覆盖。沿组的方向按方向键移动它两侧的项,每次移动组尺寸的 1%,按住 Shift 为 10%;Home、End把它前一项调到最小、最大尺寸。Resizable、ResizeGroup、ResizeItem的根元素没有语义;传入aria-label或aria-labelledby时,根元素带上role="group"。
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $z-resizable_background | 20 | 伸缩框组件中背景的z-index,保持略高于handler |
| $height-row-handler | 10px | 单个伸缩框中上下handler的高度 |
| $width-col-handler | 10px | 单个伸缩框中左右handler的宽度 |
| $width-edge-handler | 20px | 单个伸缩框中边角handler的宽度 |
| $height-edge-handler | 20px | 单个伸缩框中边角handler的高度 |
| $width-horizontal-handler | 10px | 组合伸缩框中水平方向handler的宽度 |
| $height-vertical-handler | 10px | 组合伸缩框中垂直方向handler的高度 |