利用 Zag 实现核心逻辑
Zag 中将 Checkbox 分为三个组成部分:
root:根元素,用于包裹内部元素control: 掌握器label: 标签内容我们首先在 Zag Checkbox 文档中复制 JSX 的实例代码:
import as checkbox from 34;@zag-js/checkbox"import { normalizeProps, useMachine } from "@zag-js/vue"import { defineComponent, h, Fragment, computed } from "vue"export default defineComponent({ name: "Checkbox", setup() { const [state, send] = useMachine(checkbox.machine({ id: "checkbox" })) const apiRef = computed(() => checkbox.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <label {...api.rootProps}> <span {...api.labelProps}> Input is {api.isChecked ? "checked" : "unchecked"} </span> <div {...api.controlProps} /> <input {...api.hiddenInputProps} /> </label> ) } },})
这段代码利用了 useMachine 函数创建了一个状态机,并且写了一个无样式的根本 checkbox 构造:
接下来我们为 checkbox 组件补充样式.
实现打勾动画图标想要实现丝滑的勾选框效果,最直不雅观的是打勾的动画。这个效果可以通过 SVG 或者纯 css 实现,这里我利用的是 css 来实现的。:
首先我们要想想如何实现一个勾勾的效果,✔️由一长一短两个斜边组成,那么我们只须要放置一个矩形,设置一定的旋转角度,并设置个中的两个边框,就能实现一个✔️的形状:
代码实现如下:
import { defineComponent } from "vue";import { css, cx } from "@/styled-system/css";const IconCheck = defineComponent({ props: { color: { type: String, default: css({ colorPalette: "gray" }), }, size: { type: String, default: css({ colorPalette: "gray" }), }, customCSS: { type: String, }, }, setup(props) { return () => ( <i class={cx( css({ display: "flex", alignItems: "center", justifyContent: "center", }), props.customCSS, )} > <div class={css({ position: "relative", width: "80%", height: "40%", transform: "rotate(-45deg)", })} > <div class={css({ position: "absolute", left: "0", width: "full", height: "full", borderLeft: "2px solid white", animation: "checkShort 0.15s", })} /> <div class={css({ position: "absolute", left: "0", width: "full", height: "full", borderBottom: "2px solid white", animation: "checkLong 0.5s", })} /> </div> </i> ); },});export default IconCheck;
上面这段代码中,定义了一个矩形,宽高分别为最外层容器的 80% 和 40%,transform: "rotate(-45deg)", 则设置了矩形的旋转角度,内部放置两个与矩形宽高同等的容器,分别设置 borderLeft 和 borderBottom ,这样就组成了一长一短两条线。
这里之以是须要在内部放两个容器单独设置边框,而不是直接在矩形设置边框,是为了更好的实现动画效果,是非边分别设置了两个持续韶光不同的动画 checkShort 0.15s 和 checkLong 0.5s:
checkShort: { "0%": { height:0, }, "100%":{ height: "full", }},checkLong: { "0%": { width: 0, }, "30%":{ width: 0, }, "100%": { width: "full", }}
短边从最开始就实行动画,持续韶光为长边动画的 30%,长边则在 0-30% 时不实行,30% 之后开始实行,这样就能实现短边动画实行完成后,长边动画再实行的效果。
之以是不该用 animation-delay 去延迟实行长边动画,是由于这种办法会导致动画延迟实行前,长边会先展示出来,效果就不对了。如果要利用这种办法还得单独做一些动画延迟前的隐蔽处理,会比较麻烦:
实现 Hover 效果
为了让用户更随意马虎感知勾选框是可以交互的,我们为勾选框增加未勾选和关状态的 hover 效果。
未勾选状态未勾选状态的 hover 效果,默认只有灰色边框,鼠标悬浮后变为灰色背景:
这里有个把稳点,我们鼠标悬浮在勾选框的最外层,也可以触发内层的 hover 样式,如果直策应用 hover 效果是没法做到的,这样只能鼠标悬浮在边框内才能触发。
如果我们没有利用任何样式库,实现这个效果可以通过掩护一个 鼠标是否 hover 的状态,并通过 onMouseEnter 和 onMouseLeave 来更新这个状态,再在内层根据这个状态动态渲染样式。
但这里我们可以利用 pandaCSS 的 group 选择器来实现。
首先在勾选框最外层元素增加 group 类名:
<label {...api.rootProps} class={[ css({ display: "flex", alignItems: "center", cursor: "pointer", }),+ "group", ]}>
然后在内层的 control 元素增加根本样式:
<div {...api.controlProps} class={css({ width: "24px", height: "24px", borderRadius: "8px", border: api.isChecked ? "none" : "token(colors.gray.200) solid 2px", transition: "all 0.3s", marginRight: "4px", flexShrink: "0", _groupHover: { background: "gray.200", }, })} > // ... </div>
这里的 _groupHover 即为 group 选择器,当带有 group 类名的元素触发 hover 时,内部的元素都可以利用 _groupHover 设置特定样式。这样我们就实现了前文图中的效果。
勾选状态在勾选时,会有一个蓝色色块放大渐出的效果,我们先来实现这个样式。
<Transition enterFromClass={css({ transition: "all 0.2s", transform: "scale(0.5)", opacity: "0", })} enterToClass={css({ transition: "all 0.2s", transform: "scale(1)", opacity: "1", })} leaveToClass={css({ transition: "all 0.2s", transform: "scale(0.5)", opacity: "0", })}> {api.isChecked && ( <div class={cx( props.color, css({ width: "full", height: "full", background: "colorPalette.600", borderRadius: "inherit", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.3s", }), )} > <IconCheck customCSS={css({ width: "18px", height: "18px", })} /> </div> )}</Transition>
这里我们利用 vue 中的 Transition 组件来实现动画效果,通过改变scale 和 opacity 实现元素大小和透明度的过渡动画,内部包裹着勾选的图标。
实现了勾选的效果,连续实现勾选后的 hover 样式。勾选后在 hover 时,勾选框的外层有一个与主题色相同的外层阴影效果:
这里我们依然利用 group 选择器来设置 hover 样式:
<div class={cx( props.color, css({ width: "full", height: "full", background: "colorPalette.600", borderRadius: "inherit", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.3s",+ _groupHover: {+ boxShadow:+ "0px 3px 15px 0px color-mix(in srgb, token(colors.colorPalette.600) 40%, transparent)",+ }, }), )}> <IconCheck customCSS={css({ width: "18px", height: "18px", })} ></IconCheck></div>
在阴影效果的代码中 0px 3px 15px 0px color-mix(in srgb, token(colors.colorPalette.600) 40%, transparent) ,前面几个设置阴影大小的参数很随意马虎理解,但是后面阴影颜色的实现大家可能比较陌生,单独阐明一下:
token(colors.colorPalette.600):token 是 pandaCSS 中供应的一种 token 利用方法,用于在 border,boxSahdow 这种组合多个参数的样式中引用 token 值。干系文档:panda-css.com/docs/themin…color-mix() : color-mix 则是原生 css 的函数,用于吸收两个颜色值,并返回在指定色彩模式、以指定比例稠浊后的颜色。干系文档:developer.mozilla.org/zh-CN/docs/…我这里用法的含义是在 srgb 的色彩模式下,将主题色(token(colors.colorPalette.600)) 与透明色(transparent),以 40% 的比例进行稠浊,终极合成的颜色便是 40% 透明度的主题色。 color-mix() 函数是 pandaCSS 中推举用户用于为内置颜色设置透明度的方法,除此以外并没有创造其他更简便的办法。
完成双向绑定末了我们完善一下勾选框的双向绑定逻辑逻辑,
通过 Zag 中的 onCheckedChange 合营 emit 可以实现 Zag 内部状态向外传递通过监听 propmodelValue 的改变,实行 apiRef.value.setChecked 可以实现外部状态向 Zag 内通报实现的代码如下:
const [state, send] = useMachine( checkbox.machine({ id: useId("checkbox"), onCheckedChange(detail) { emit("update:modelValue", detail.checked); }, }),);const apiRef = computed(() => checkbox.connect(state.value, send, normalizeProps),);watch( () => props.modelValue, () => { apiRef.value.setChecked(props.modelValue); },);
✅ 利用 Vue+Zag+PandaCSS 实现一个超丝滑的勾选框组件原文链接:https://juejin.cn/post/7295954109404463155