197 lines
6.5 KiB
Vue
197 lines
6.5 KiB
Vue
<script lang="ts" setup>
|
|
import {
|
|
Preview,
|
|
SectionTitle,
|
|
SectionContent,
|
|
PreviewCode,
|
|
} from "@/components/docs";
|
|
import { Box } from "@/components/ui/box";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ChevronLeft, ChevronRight } from "@lucide/vue";
|
|
</script>
|
|
|
|
<template>
|
|
<div id="preview" class="-mt-20">
|
|
<Preview>
|
|
<template #preview>
|
|
<div class="justify-center items-center flex gap-2">
|
|
<Box as-child>
|
|
<Button variant="ghost" class="me-2 px-2">
|
|
<ChevronLeft class="size-5" />
|
|
</Button>
|
|
</Box>
|
|
<Box as-child>
|
|
<Button variant="ghost" class="px-2">
|
|
<ChevronRight class="size-5" />
|
|
</Button>
|
|
</Box>
|
|
</div>
|
|
</template>
|
|
<template #code>
|
|
<PreviewCode>
|
|
{{ `
|
|
<div class="justify-center items-center flex gap-2">
|
|
<Box as-child>
|
|
<Button variant="ghost" class="me-2 px-2">
|
|
<ChevronLeft class="size-5" />
|
|
</Button>
|
|
</Box>
|
|
<Box as-child>
|
|
<Button variant="ghost" class="px-2">
|
|
<ChevronRight class="size-5" />
|
|
</Button>
|
|
</Box>
|
|
</div>
|
|
` }}
|
|
</PreviewCode>
|
|
</template>
|
|
</Preview>
|
|
</div>
|
|
|
|
<div id="installation">
|
|
<SectionTitle>Installation</SectionTitle>
|
|
<SectionContent>
|
|
By using <code>Slot</code> at the root, you ensure that props are correctly merged whether
|
|
you're using it as a direct wrapper or an <code>asChild</code> bridge.
|
|
</SectionContent>
|
|
<SectionContent>
|
|
Copy and paste the following code into your project.
|
|
</SectionContent>
|
|
|
|
<PreviewCode title="components/ui/slot/index.ts">
|
|
{{ `import {
|
|
defineComponent,
|
|
h,
|
|
cloneVNode,
|
|
Fragment,
|
|
isVNode,
|
|
type VNode,
|
|
type PropType,
|
|
} from "vue";
|
|
import { calculateSlot, flattenItems, type AnyProps } from "./slot";
|
|
|
|
export const Slot = defineComponent({
|
|
name: "Slot",
|
|
inheritAttrs: false,
|
|
props: {
|
|
children: {
|
|
type: [Object, Array] as PropType<any>,
|
|
},
|
|
},
|
|
setup(props, { attrs, slots }) {
|
|
return () => {
|
|
const raw = props.children ?? slots.default?.();
|
|
const isValidVNode = (item: any): item is VNode => isVNode(item) && typeof item.type !== "symbol";
|
|
|
|
const items = flattenItems<VNode>(
|
|
raw as any,
|
|
(item) => isVNode(item) && item.type === Fragment,
|
|
(item) => (isVNode(item) && Array.isArray(item.children) ? (item.children as VNode[]) : [])
|
|
).filter(isValidVNode);
|
|
|
|
const result = calculateSlot<VNode>({
|
|
props: attrs as AnyProps,
|
|
items,
|
|
isValid: isValidVNode,
|
|
getProps: (item) => (item.props as AnyProps) || {},
|
|
getChildren: (item) => item.children,
|
|
});
|
|
|
|
if (result.type === "wrapper") {
|
|
return h("div", result.props, result.children as any);
|
|
}
|
|
|
|
const target = result.target;
|
|
return cloneVNode(target, result.props, false);
|
|
};
|
|
},
|
|
});
|
|
|
|
export { Slot as Root };` }}
|
|
</PreviewCode>
|
|
<PreviewCode title="components/ui/slot/slot.ts">
|
|
{{ `export type AnyProps = Record<string, any>;
|
|
|
|
export interface SlotParams<T> {
|
|
props: AnyProps;
|
|
items: T[];
|
|
isValid: (item: T) => boolean;
|
|
getProps: (item: T) => AnyProps;
|
|
getChildren: (item: T) => any;
|
|
}
|
|
|
|
export type SlotResult<T> =
|
|
| { type: "slotted"; target: T; props: AnyProps; children: any }
|
|
| { type: "wrapper"; target: "div"; props: AnyProps; children: T[] };
|
|
|
|
export function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
const result: AnyProps = { ...childProps };
|
|
for (const key in slotProps) {
|
|
const slotValue = slotProps[key];
|
|
const isHandler = /^on[A-Z]/.test(key);
|
|
if (isHandler) {
|
|
const childValue = childProps[key];
|
|
if (typeof slotValue === "function" && typeof childValue === "function") {
|
|
result[key] = (...args: any[]) => {
|
|
childValue(...args);
|
|
slotValue(...args);
|
|
};
|
|
} else if (slotValue) {
|
|
result[key] = slotValue;
|
|
}
|
|
continue;
|
|
}
|
|
if (key === "class" || key === "className") {
|
|
const slotClasses = (slotValue || "").split(/\\s+/);
|
|
const childClasses = (childProps.class || childProps.className || "").split(/\\s+/);
|
|
const combined = Array.from(new Set([...slotClasses, ...childClasses])).filter(Boolean).join(" ");
|
|
result[key] = combined;
|
|
continue;
|
|
}
|
|
if (key === "style") {
|
|
result[key] = { ...slotValue, ...childProps.style };
|
|
continue;
|
|
}
|
|
if (childProps[key] === undefined) {
|
|
result[key] = slotValue;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function flattenItems<T>(items: T | T[], isFragment: (item: T) => boolean, getChildren: (item: T) =>
|
|
T
|
|
| T[]): T[] {
|
|
const result: T[] = [];
|
|
const list = Array.isArray(items) ? items : [items];
|
|
list.forEach((item) => {
|
|
if (item === null || item === undefined) return;
|
|
if (isFragment(item)) {
|
|
const children = getChildren(item);
|
|
result.push(...flattenItems(children, isFragment, getChildren));
|
|
} else {
|
|
result.push(item);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export function calculateSlot<T>(params: SlotParams<T>): SlotResult<T> {
|
|
const { props, items, isValid, getProps, getChildren } = params;
|
|
if (items.length === 1) {
|
|
const primary = items[0];
|
|
if (isValid(primary)) {
|
|
return {
|
|
type: "slotted",
|
|
target: primary,
|
|
props: mergeProps(props, getProps(primary)),
|
|
children: getChildren(primary),
|
|
};
|
|
}
|
|
}
|
|
return { type: "wrapper", target: "div", props: props, children: items };
|
|
}` }}
|
|
</PreviewCode>
|
|
</div>
|
|
</template>
|