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
19 changes: 15 additions & 4 deletions src/components/changelog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ describe('the changelog on the server', () => {
expect(renderToStaticMarkup(<ChangelogView releases={[]} />)).toBe('');
});

it('offers a filter for each part, as toggle buttons that are all off', () => {
it('labels the filter group and marks All parts as the default selection', () => {
expect(html).toContain('id="marfa-cl-filter-label" class="marfa-cl-filter-label">Filter by part');
expect(html).toContain('role="group" aria-labelledby="marfa-cl-filter-label"');
expect(html).toMatch(/<button[^>]*aria-pressed="true"[^>]*>All parts<\/button>/);
expect(html.match(/<button[^>]*aria-pressed="false"[^>]*>(Server|CLI)<\/button>/g)).toHaveLength(2);
});
});
Expand All @@ -103,18 +106,26 @@ describe('the part filters', () => {
const button = (name: string) => [...container.querySelectorAll('button')].find((b) => b.textContent === name)!;
const hiddenParts = () => [...container.querySelectorAll<HTMLElement>('.marfa-cl-part')].filter((p) => p.hidden).map((p) => p.getAttribute('aria-label'));

it('shows only the selected parts, and every part again when none is selected', () => {
it('shows only selected parts, and restores All parts when the filter is cleared', () => {
expect(button('All parts').getAttribute('aria-pressed')).toBe('true');
act(() => button('Server').click());
expect(button('Server').getAttribute('aria-pressed')).toBe('true');
expect(button('All parts').getAttribute('aria-pressed')).toBe('false');
expect(hiddenParts()).toEqual(['CLI', 'CLI']);

act(() => button('CLI').click());
act(() => button('All parts').click());
expect(button('All parts').getAttribute('aria-pressed')).toBe('true');
expect(button('Server').getAttribute('aria-pressed')).toBe('false');
expect(hiddenParts()).toEqual([]);
});

it('returns to All parts when every part is selected', () => {
act(() => button('Server').click());
act(() => button('CLI').click());
expect(button('All parts').getAttribute('aria-pressed')).toBe('true');
expect(button('Server').getAttribute('aria-pressed')).toBe('false');
expect(button('CLI').getAttribute('aria-pressed')).toBe('false');
expect(hiddenParts()).toEqual([]);
expect(container.querySelectorAll('[aria-pressed="true"]')).toHaveLength(0);
});

it('keeps every release heading while a filter hides its parts, and says when nothing is left', () => {
Expand Down
29 changes: 23 additions & 6 deletions src/components/changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,35 @@ export function ChangelogView({ releases }: { releases: Release[] }) {
setSelected((current) => {
const next = new Set(current);
if (!next.delete(name)) next.add(name);
return next;
return next.size === parts.length ? new Set() : next;
});

return (
<div className="marfa-cl">
{parts.length > 1 ? (
<div className="marfa-cl-filters" role="group" aria-label="Show changes to">
{parts.map((name) => (
<button key={name} type="button" className="marfa-ix-button" aria-pressed={selected.has(name)} onClick={() => toggle(name)}>
{name}
<div className="marfa-cl-filters">
<span id="marfa-cl-filter-label" className="marfa-cl-filter-label">Filter by part</span>
<div className="marfa-cl-filter-options" role="group" aria-labelledby="marfa-cl-filter-label">
<button
type="button"
className="marfa-cl-filter-button"
aria-pressed={selected.size === 0}
onClick={() => setSelected(new Set())}
>
All parts
</button>
))}
{parts.map((name) => (
<button
key={name}
type="button"
className="marfa-cl-filter-button"
aria-pressed={selected.has(name)}
onClick={() => toggle(name)}
>
{name}
</button>
))}
</div>
</div>
) : null}
{releases.map((release, index) => (
Expand Down
43 changes: 39 additions & 4 deletions src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -517,15 +517,50 @@ html > body[data-scroll-locked] {
line-height: 1.6;
}

/* The changelog: a row of part filters, then one entry for each release. The
filters are the buttons of an interactive figure (marfa-ix-button). A part
is hidden, never removed, so a release keeps its heading and summary. */
/* The changelog filters sit inline with a visible label, like a filter rather
than page navigation. A part is hidden, never removed, so a release keeps
its heading and summary. */
.marfa-cl-filters {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
align-items: center;
gap: 0.5rem 1rem;
margin: 1.5rem 0 0.5rem;
}
.marfa-cl-filter-label {
color: var(--color-fd-foreground);
font-size: 0.9375rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.marfa-cl-filter-options {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.375rem;
}
.marfa-cl-filter-button {
min-height: 2.25rem;
padding: 0.3125rem 0.875rem;
border: 1px solid var(--color-fd-border);
border-radius: 9999px;
background: transparent;
color: var(--color-fd-muted-foreground);
font-size: 0.875rem;
font-weight: 550;
line-height: 1.25rem;
white-space: nowrap;
transition: background-color 0.15s, border-color 0.15s, color 0.15s;
}
.marfa-cl-filter-button:hover {
border-color: color-mix(in oklab, var(--color-fd-foreground) 35%, var(--color-fd-border));
color: var(--color-fd-foreground);
}
.marfa-cl-filter-button[aria-pressed='true'] {
border-color: var(--color-fd-foreground);
background: var(--color-fd-foreground);
color: var(--color-fd-background);
}
.marfa-cl-release {
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-fd-border);
Expand Down
Loading