Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/website/src/_state/component-statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ export const componentsStatuses: ComponentKitsStatuses = {
Tabs: 'Planned',
Toast: 'Planned',
Toggle: 'Planned',
Tooltip: 'Planned',
Tooltip: 'Planned'
},
headless: {
Accordion: 'Planned',
Accordion: 'Ready',
Autocomplete: 'Draft',
Carousel: 'Planned',
Popover: 'Planned',
Select: 'Draft',
Tabs: 'Ready',
Toggle: 'Planned',
Tooltip: 'Planned',
},
Tooltip: 'Planned'
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const AnatomyTable = component$(({ propDescriptors }: AnatomyTableProps)
{propDescriptors?.map((propDescriptor) => {
return (
<tr key={propDescriptor.name}>
<td class="prose prose-sm py-3 pl-4 align-baseline sm:pl-0 ">
<td class="prose prose-sm py-3 pl-2 pr-2 align-center sm:pl-0 md:align-baseline">
<code>{propDescriptor.name}</code>
</td>
<td class="py-3 align-baseline">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const HeroAccordion = component$(() => {
<div class="w-full flex justify-center" q:slot="actualComponent">
<AccordionRoot
animated
enhance={true}
class="bg-gray-100 dark:bg-gray-700 rounded-sm border-slate-200 dark:border-gray-600 border-[1px] box-border w-[min(400px,_100%)]"
>
<AccordionItem>
Expand Down Expand Up @@ -589,63 +590,100 @@ export const OnFocusIndexChange = component$(() => {
);
});

export const DynamicAccordion = component$(() => {
const itemStore = useStore<number[]>([1, 2]);
interface DynamicAccordionProps {
itemIndexToDelete?: number;
itemIndexToAdd?: number;
itemsLength: number;
}

return (
<PreviewCodeExample>
<div class="w-full flex flex-col items-center" q:slot="actualComponent">
<AccordionRoot class="bg-gray-100 dark:bg-gray-700 rounded-sm border-slate-200 dark:border-gray-600 border-[1px] border-t-[0px] box-border w-[min(400px,_100%)]">
{itemStore.map((itemNumber, index) => {
return (
<AccordionItem key={index}>
<AccordionTrigger class="bg-violet-50 hover:bg-violet-100 dark:bg-gray-700 px-4 py-2 w-full dark:hover:bg-gray-800 text-left flex items-center justify-between group aria-expanded:rounded-none border-t-[1px] border-slate-200 dark:border-gray-600">
<span>Trigger {itemNumber}</span>
<span class="pl-2 flex">
<p class="group-aria-expanded:transform group-aria-expanded:rotate-45 scale-150">
+
</p>
</span>
</AccordionTrigger>
<AccordionContent class="bg-violet-200 dark:bg-gray-900 p-4 border-t-[1px] dark:border-gray-600 border-slate-200">
Content {itemNumber}
</AccordionContent>
</AccordionItem>
);
})}
</AccordionRoot>
export const DynamicAccordion = component$(
({ itemsLength = 3 }: DynamicAccordionProps) => {
const itemIndexToAdd = useSignal<string>('0');
const itemIndexToDelete = useSignal<string>('0');

// start off with some items
const items = [];
const newItem = { label: 'New Item', id: Math.random() };

for (let i = 0; i < itemsLength; i++) {
items.push({
label: `Original Item ${i + 1}`,
id: Math.random()
});
}

const itemStore = useStore<{ label: string; id: number }[]>(items);

return (
<PreviewCodeExample>
<div class="w-full flex flex-col items-center" q:slot="actualComponent">
<div class="flex gap-4">
<label class="flex flex-col-reverse mb-4 items-center text-center">
<input
class="rounded-md px-2 max-w-[50px] bg-[#374151]"
type="text"
bind:value={itemIndexToAdd}
/>
<span>Index to Add</span>
</label>

<div class="flex flex-col sm:flex-row gap-2 md:gap-4">
<button
style={{ color: 'green', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length < 4) {
itemStore.push(itemStore.length + 1);
}
}}
>
<strong>Add Item</strong>
</button>
<label class="flex flex-col-reverse mb-4 items-center text-center">
<input
class="rounded-md px-2 max-w-[50px] bg-[#374151]"
type="text"
bind:value={itemIndexToDelete}
/>
<span>Index to Delete</span>
</label>
</div>

<button
style={{ color: 'red', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length > 1) {
itemStore.pop();
}
}}
>
<strong>Remove Item</strong>
</button>
<AccordionRoot class="bg-gray-100 dark:bg-gray-700 rounded-sm border-slate-200 dark:border-gray-600 border-[1px] border-t-[0px] box-border w-[min(400px,_100%)]">
{itemStore.map(({ label, id }, index) => {
return (
<AccordionItem id={`${id}`} key={id}>
<AccordionHeader>
<AccordionTrigger class="bg-violet-50 hover:bg-violet-100 dark:bg-gray-700 px-4 py-2 w-full dark:hover:bg-gray-800 text-left flex items-center justify-between group aria-expanded:rounded-none border-t-[1px] border-slate-200 dark:border-gray-600">
{label}
</AccordionTrigger>
</AccordionHeader>
<AccordionContent class="bg-violet-200 dark:bg-gray-900 p-4 border-t-[1px] dark:border-gray-600 border-slate-200">
index: {index}
</AccordionContent>
</AccordionItem>
);
})}
</AccordionRoot>
<div class="flex gap-2 md:gap-4">
<button
style={{ color: 'green', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length < 6) {
itemStore.splice(parseInt(itemIndexToAdd.value), 0, newItem);
}
}}
>
<strong>Add Item</strong>
</button>
<button
style={{ color: 'red', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length > 2) {
itemStore.splice(parseInt(itemIndexToDelete.value), 1);
}
}}
>
<strong>Remove Item</strong>
</button>
</div>
</div>
</div>

<div q:slot="codeExample">
<Slot />
</div>
</PreviewCodeExample>
);
});
<div q:slot="codeExample">
<Slot />
</div>
</PreviewCodeExample>
);
}
);

export function SVG(props: QwikIntrinsicElements['svg'], key: string) {
return (
Expand Down
100 changes: 78 additions & 22 deletions apps/website/src/routes/docs/headless/(components)/accordion/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -583,55 +583,90 @@ By default, when using the `AccordionHeader` component, it's rendered as an `h3`

<DynamicAccordion>
```tsx
<AccordionRoot class="accordion-root">
{itemStore.map((itemNumber, index) => {
export const DynamicAccordion = component$(
({ itemsLength = 3}: DynamicAccordionProps) => {

const itemIndexToAdd = useSignal<string>('0');
const itemIndexToDelete = useSignal<string>('0');

// start off with some items
const items = [];
const newItem = { label: 'New Item', id: Math.random() };

for(let i = 0; i < itemsLength; i++) {
items.push({
label: `Original Item ${i + 1}`,
id: Math.random()
});
}

const itemStore = useStore<{ label: string, id: number }[]>(items);

return (
<div>
<label>
<input bind:value={itemIndexToAdd} />
<span>Index to Add</span>
</label>

<label>
<input bind:value={itemIndexToDelete} />
<span>Index to Delete</span>
</label>
</div>

<AccordionRoot class="dynamic-root">
{itemStore.map(({label, id}, index) => {
return (
<AccordionItem key={index}>
<AccordionTrigger class="accordion-trigger"><span>Trigger {itemNumber}</span>
<span class="accordion-icon">
<p>
+
</p>
</span>
</AccordionTrigger>
<AccordionContent class="accordion-content">
Content {itemNumber}
</AccordionContent>
<AccordionItem id={`${id}`} key={id}>
<AccordionHeader>
<AccordionTrigger class="dynamic-trigger">
{label}
</AccordionTrigger>
</AccordionHeader>
<AccordionContent class="dynamic-content">index: {index}</AccordionContent>
</AccordionItem>
)
);
})}
</AccordionRoot>

<div>
<button
style={{ color: 'green', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length < 4) {
itemStore.push(itemStore.length + 1);
if (itemStore.length > 2) {
itemStore.splice(parseInt(itemIndexToAdd.value), 1);
}
}}
>
<strong>Add Item</strong>
</button>

<button
style={{ color: 'red', marginTop: '1rem' }}
onClick$={() => {
if (itemStore.length > 1) {
itemStore.pop();
if (itemStore.length < 6) {
itemStore.splice(parseInt(itemIndexToAdd.value), 0, newItem);
}
}}
>
<strong>Remove Item</strong>
</button>
</div>

)}
);
```
</DynamicAccordion>

You can embrace reactivity, using signals, stores, and however else you'd like to use the Accordion with dynamic behavior.

When an Accordion Item is removed, a **Visible Task** runs that will clean up the DOM node in the browser, ensuring that you stay clear of race condition or memory leak issues.
When an Accordion Item is removed, a [Visible Task](https://qwik.builder.io/docs/components/tasks/#usevisibletask) runs that will clean up the DOM node in the browser, ensuring that you stay clear of race condition or memory leak issues.

> You can add or remove something at any index and the focus order will adhere to the DOM hierarchy!

<div class="my-4">
If you'd prefer to add your own <strong>id</strong> to the Accordion Item with dynamic behavior, you can add the `id` prop to the Accordion Item. This can be useful when you'd like the id value to be sync with your custom logic.
</div>

By default, the Accordion Item has a locally scoped id with Qwik's `useId` hook. All children elements will be prefixed by its respective item id, followed by a dash and the element. For example, `{id}-trigger`.

## Accessibility

Expand Down Expand Up @@ -713,6 +748,27 @@ propDescriptors={[

<br />

### Accordion Item

<APITable
propDescriptors={[
{
name: 'id',
type: 'string',
description:
'Allows the consumer to supply their own id attribute for the item and its descendants.',
},
{
name: 'defaultValue',
type: 'boolean',
description:
'Determines whether the Accordion Item will open by default.',
},
]}
/>

<br />

### Accordion Header

<APITable
Expand Down
Loading