mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
UI: Support specifying a custom value for Accordion
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Support specifying a custom `value` for `Accordion`
|
||||
@@ -6,37 +6,60 @@ import type {
|
||||
} from '@radix-ui/react-accordion';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { Check, ChevronRight, Link as LinkIcon } from 'lucide-react';
|
||||
import type * as React from 'react';
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
forwardRef,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useCopyButton } from '@/utils/use-copy-button';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
|
||||
return (value) => {
|
||||
refs.forEach((ref) => {
|
||||
if (typeof ref === 'function') {
|
||||
ref(value);
|
||||
} else if (ref !== null) {
|
||||
ref.current = value;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const Accordions = forwardRef<
|
||||
HTMLDivElement,
|
||||
| Omit<AccordionSingleProps, 'value' | 'onValueChange'>
|
||||
| Omit<AccordionMultipleProps, 'value' | 'onValueChange'>
|
||||
>(({ type = 'single', className, defaultValue, ...props }, ref) => {
|
||||
const [value, setValue] = useState<string | string[]>(
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const composedRef = mergeRefs(ref, rootRef);
|
||||
const [value, setValue] = useState<string | string[]>(() =>
|
||||
type === 'single' ? (defaultValue ?? '') : (defaultValue ?? []),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.location.hash.substring(1);
|
||||
const element = rootRef.current;
|
||||
if (!element || id.length === 0) return;
|
||||
|
||||
if (id.length > 0)
|
||||
setValue((prev) => (typeof prev === 'string' ? id : [id, ...prev]));
|
||||
const selected = document.getElementById(id);
|
||||
if (!selected || !element.contains(selected)) return;
|
||||
const value = selected.getAttribute('data-accordion-value');
|
||||
|
||||
if (value)
|
||||
setValue((prev) => (typeof prev === 'string' ? value : [value, ...prev]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// @ts-expect-error -- Multiple types
|
||||
<AccordionPrimitive.Root
|
||||
type={type}
|
||||
ref={ref}
|
||||
ref={composedRef}
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
collapsible={type === 'single' ? true : undefined}
|
||||
@@ -53,41 +76,52 @@ Accordions.displayName = 'Accordions';
|
||||
|
||||
export const Accordion = forwardRef<
|
||||
HTMLDivElement,
|
||||
Omit<ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>, 'value'> & {
|
||||
title: string;
|
||||
Omit<
|
||||
ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>,
|
||||
'value' | 'title'
|
||||
> & {
|
||||
title: string | ReactNode;
|
||||
value?: string;
|
||||
}
|
||||
>(({ title, className, id, children, ...props }, ref) => {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
// Use `id` instead if presents
|
||||
value={id ?? title}
|
||||
className={cn('group/accordion relative scroll-m-20', className)}
|
||||
{...props}
|
||||
>
|
||||
<AccordionPrimitive.Header
|
||||
id={id}
|
||||
className="not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent"
|
||||
>(
|
||||
(
|
||||
{ title, className, id, value = String(title), children, ...props },
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
value={value}
|
||||
className={cn('group/accordion relative scroll-m-20', className)}
|
||||
{...props}
|
||||
>
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none">
|
||||
<ChevronRight className="size-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 group-data-[state=open]/accordion:rotate-90" />
|
||||
{title}
|
||||
</AccordionPrimitive.Trigger>
|
||||
{id ? <CopyButton id={id} /> : null}
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Content className="overflow-hidden data-[state=closed]:animate-fd-accordion-up data-[state=open]:animate-fd-accordion-down">
|
||||
<div className="px-4 pb-2 text-[15px] prose-no-margin">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
</AccordionPrimitive.Item>
|
||||
);
|
||||
});
|
||||
<AccordionPrimitive.Header
|
||||
id={id}
|
||||
data-accordion-value={value}
|
||||
className="not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent"
|
||||
>
|
||||
<AccordionPrimitive.Trigger className="flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none">
|
||||
<ChevronRight className="size-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 group-data-[state=open]/accordion:rotate-90" />
|
||||
{title}
|
||||
</AccordionPrimitive.Trigger>
|
||||
{id ? <CopyButton id={id} /> : null}
|
||||
</AccordionPrimitive.Header>
|
||||
<AccordionPrimitive.Content className="overflow-hidden data-[state=closed]:animate-fd-accordion-up data-[state=open]:animate-fd-accordion-down">
|
||||
<div className="px-4 pb-2 text-[15px] prose-no-margin">
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
</AccordionPrimitive.Item>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function CopyButton({ id }: { id: string }): React.ReactElement {
|
||||
function CopyButton({ id }: { id: string }) {
|
||||
const [checked, onClick] = useCopyButton(() => {
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = id;
|
||||
|
||||
void navigator.clipboard.writeText(url.toString());
|
||||
return navigator.clipboard.writeText(url.toString());
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user