Skip to content

Migrate accordion #2721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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: 7 additions & 1 deletion src/lib/holocene/accordion/accordion-group.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<script lang="ts">
import type { Snippet } from 'svelte';

const { children }: { children: Snippet } = $props();
</script>

<div class="*:border-t-0 [&>*:first-child]:border-t">
<slot />
{@render children()}
</div>
28 changes: 14 additions & 14 deletions src/lib/holocene/accordion/accordion.stories.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import { iconNames } from '$lib/holocene/icon';

import Accordion from './accordion.svelte';
import Accordion, { type Props } from './accordion.svelte';

export const meta = {
export const meta: Meta<Props> = {
title: 'Accordion',
component: Accordion,
args: {
Expand All @@ -16,18 +16,14 @@
error: '',
},
argTypes: {
title: { name: 'Title', control: 'text' },
subtitle: { name: 'Subtitle', control: 'text' },
open: { name: 'Open', control: 'boolean' },
expandable: { name: 'Expandable', control: 'boolean' },
error: { name: 'Error', control: 'text' },
icon: {
name: 'Icon',
control: 'select',
options: iconNames,
},
title: { control: 'text' },
subtitle: { control: 'text' },
open: { control: 'boolean' },
expandable: { control: 'boolean' },
error: { control: 'text' },
icon: { control: 'select', options: iconNames },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: Storybook might set up most of these automatically now with Meta<AccordionProps>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit less sure about this one... Let me know what you think we should do here or if you have any docs that are helpful pointing me in the right direction.

Copy link
Contributor

@andrewzamojc andrewzamojc Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change needed:

No worries. This is a sidetrack and doesn't need to be considered in this PR. For interest sake or future work, I found this bit on storybook choosing controls automatically https://storybook.js.org/docs/essentials/controls#choosing-the-control-type

},
} satisfies Meta<Accordion>;
};
</script>

<script lang="ts">
Expand All @@ -41,7 +37,11 @@

<Template let:args>
<div class="flex flex-col gap-2">
<Accordion {...args} onToggle={action('onToggle')}>
<Accordion
{...args}
onToggle={action('onToggle')}
onclick={() => window.alert('CLICKED')}
>
<p>Accordion Content</p>
</Accordion>
<AccordionGroup>
Expand Down
80 changes: 45 additions & 35 deletions src/lib/holocene/accordion/accordion.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';

import { twMerge as merge } from 'tailwind-merge';
import { v4 } from 'uuid';

import Badge from '$lib/holocene/badge.svelte';
import type { IconName } from '$lib/holocene/icon';
import Icon from '$lib/holocene/icon/icon.svelte';

interface $$Props extends HTMLAttributes<HTMLDivElement> {
<script module>
export type Props = HTMLAttributes<HTMLDivElement> & {
title: string;
id?: string;
subtitle?: string;
Expand All @@ -19,19 +10,38 @@
onToggle?: () => void;
'data-testid'?: string;
class?: string;
}
summary?: Snippet;
action?: Snippet;
children?: Snippet;
};
</script>

<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';

import type { Snippet } from 'svelte';
import { twMerge as merge } from 'tailwind-merge';
import { v4 } from 'uuid';

export let title: string;
export let id: string = v4();
export let subtitle = '';
export let icon = null;
export let open = false;
export let expandable = true;
export let error = '';
export let onToggle = () => {};
import Badge from '$lib/holocene/badge.svelte';
import type { IconName } from '$lib/holocene/icon';
import Icon from '$lib/holocene/icon/icon.svelte';

let className = '';
export { className as class };
let {
title,
id = v4(),
subtitle = '',
icon = null,
open = false,
expandable = true,
error = '',
onToggle = () => {},
class: className = '',
summary,
action,
children,
...restProps
}: Props = $props();

const toggleAccordion = () => {
open = !open;
Expand All @@ -42,15 +52,15 @@
{#if expandable}
<div
class={merge('surface-primary w-full border border-subtle', className)}
{...$$restProps}
{...restProps}
>
<button
id="{id}-trigger"
aria-expanded={open}
aria-controls="{id}-content"
class="flex w-full flex-col p-4 focus-visible:bg-interactive-secondary-hover focus-visible:outline-none"
type="button"
on:click={toggleAccordion}
onclick={toggleAccordion}
>
<div class="flex w-full flex-row items-center justify-between gap-2">
<div class="flex w-full items-center gap-2">
Expand All @@ -59,21 +69,21 @@
{title}
</h3>
<div class="text-secondary max-sm:hidden">
<slot name="summary" />
{@render summary?.()}
</div>
</div>
<div
class="flex flex-row items-center gap-2 pr-2"
on:click|stopPropagation
on:keyup|stopPropagation
onclick={(e) => e.stopPropagation()}
onkeyup={(e) => e.stopPropagation()}
role="none"
Comment on lines +77 to +78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will break anywhere we're using click or keyup handlers like <Accordion on:click={doSomething} />. The old syntax was forwarding the listener down to this div element, this new syntax is just calling stopPropagation(), but doesn't account for if an onclick or onkeyup callback function is passed to Accordion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this spot, but rather than stopping propagation, I tend to preventDefault. It sort of marks it as handled, so up the chain, a handler can check if (e.defaultPrevented).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ya know what, I read stopPropagation as preventDefault hah, so I think this is totally fine. I think we were getting double click events or something when a custom action was passed in, hence why the stopPropagation directives were added.

Copy link
Contributor Author

@GraceGardner GraceGardner Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rossedfort Tested this by adding an alert in an onclick event and it seems to be working fine. I left the alert on the first accordion in storybook if you want to check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GraceGardner I tested and it seems to be working. Let's remove the alert before merging though. Instead, we can add action('onclick')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, thanks Ross

>
<slot name="action" />
{@render action?.()}
</div>
<Icon class="shrink-0" name={open ? 'chevron-up' : 'chevron-down'} />
</div>
<div class="text-secondary sm:hidden">
<slot name="summary" />
{@render summary?.()}
</div>
<p class="flex items-center">
{#if error}
Expand All @@ -90,11 +100,11 @@
class="mt-4 block w-full p-4"
class:hidden={!open}
>
<slot />
{@render children?.()}
</div>
</div>
{:else}
<div class="surface-primary w-full border border-subtle p-4" {...$$restProps}>
<div class="surface-primary w-full border border-subtle p-4" {...restProps}>
<div class="flex w-full flex-col">
<div class="flex w-full flex-row items-center justify-between gap-2">
<div class="flex w-full items-center gap-2">
Expand All @@ -103,15 +113,15 @@
{title}
</h3>
<div class="text-secondary max-sm:hidden">
<slot name="summary" />
{@render summary?.()}
</div>
</div>
<div class="flex flex-row items-center gap-2 pr-2">
<slot name="action" />
{@render action?.()}
</div>
</div>
<div class="text-secondary sm:hidden">
<slot name="summary" />
{@render summary?.()}
</div>
<p class="flex items-center">
{#if error}
Expand All @@ -122,7 +132,7 @@
</div>

<div class="mt-6 block w-full" class:hidden={!open}>
<slot />
{@render children?.()}
</div>
</div>
{/if}
Loading