Skip to content

Commit 3548ea7

Browse files
Sort button (#160)
1 parent 156ca1c commit 3548ea7

2 files changed

Lines changed: 259 additions & 20 deletions

File tree

packages/docs/src/components/ChartTabs.astro

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,36 +92,38 @@ const panel3Id = `${sectionId}-panel-3`
9292
.chart-tabs-container {
9393
margin-top: 1.5em;
9494
margin-bottom: 2em;
95-
padding: 1em 1.25em;
96-
border: 1px solid var(--ft-border);
97-
border-radius: 12px;
98-
background: var(--ft-bg-muted);
9995
}
10096

10197
.chart-tablist {
10298
display: flex;
103-
gap: 8px;
104-
margin: 0 0 1em 0;
99+
gap: 4px;
100+
margin: 0;
105101
padding: 0;
102+
border-bottom: 1px solid var(--ft-border);
106103
}
107104

108105
.tab {
109-
padding: 8px 16px;
106+
padding: 8px 18px;
110107
font-size: 14px;
111108
font-weight: 500;
112109
color: var(--ft-muted);
113110
background: transparent;
114-
border: none;
115-
border-radius: 8px;
111+
border: 1px solid transparent;
112+
border-bottom: none;
113+
border-radius: 8px 8px 0 0;
116114
cursor: pointer;
117115
font-family: inherit;
116+
margin-bottom: -1px;
118117
transition:
119118
color 0.2s,
120-
background-color 0.2s;
119+
background-color 0.2s,
120+
border-color 0.2s;
121121
}
122122

123123
.tab:hover {
124124
color: var(--ft-text);
125+
background: var(--ft-bg-muted);
126+
border-color: var(--ft-border);
125127
}
126128

127129
.tab:focus-visible {
@@ -132,15 +134,24 @@ const panel3Id = `${sectionId}-panel-3`
132134
.tab.active {
133135
background: var(--ft-accent);
134136
color: white;
137+
border-color: var(--ft-accent);
138+
/* covers the tablist border-bottom, merging tab into panel */
139+
border-bottom: 1px solid var(--ft-bg);
135140
}
136141

137142
:global(html.dark) .tab.active {
138143
color: var(--ft-bg);
144+
border-bottom-color: var(--ft-bg);
139145
}
140146

141147
.chart-tabpanels {
142148
position: relative;
143149
min-height: 320px;
150+
border: 1px solid var(--ft-border);
151+
border-top: none;
152+
border-radius: 0 0 12px 12px;
153+
padding: 0.75em 1.25em 1em;
154+
background: var(--ft-bg);
144155
}
145156

146157
.chart-tabpanel[hidden] {

packages/docs/src/components/ComparisonBarChart.astro

Lines changed: 238 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,52 @@ const chartPayload = JSON.stringify({ data, valueFormat })
1212
---
1313

1414
<div class="comparison-chart-container">
15-
<h3 class="comparison-chart-title">{title}</h3>
15+
<div class="comparison-chart-header">
16+
<h3 class="comparison-chart-title">{title}</h3>
17+
<details class="sort-dropdown">
18+
<summary class="sort-trigger">
19+
<svg
20+
class="sort-icon"
21+
width="14"
22+
height="14"
23+
viewBox="0 0 14 14"
24+
fill="none"
25+
aria-hidden="true"
26+
>
27+
<path
28+
d="M10 5L7 2 4 5"
29+
stroke="currentColor"
30+
stroke-width="1.5"
31+
stroke-linecap="round"
32+
stroke-linejoin="round"></path>
33+
<path
34+
d="M4 9l3 3 3-3"
35+
stroke="currentColor"
36+
stroke-width="1.5"
37+
stroke-linecap="round"
38+
stroke-linejoin="round"></path>
39+
</svg>
40+
<span class="sort-label">Default</span>
41+
</summary>
42+
<menu class="sort-menu">
43+
<li>
44+
<button class="sort-option active" data-sort="default" type="button"
45+
>Default</button
46+
>
47+
</li>
48+
<li>
49+
<button class="sort-option" data-sort="asc" type="button"
50+
>↑ Low–High</button
51+
>
52+
</li>
53+
<li>
54+
<button class="sort-option" data-sort="desc" type="button"
55+
>↓ High–Low</button
56+
>
57+
</li>
58+
</menu>
59+
</details>
60+
</div>
1661
<div class="comparison-chart-wrapper" data-comparison={chartPayload}>
1762
<svg class="comparison-bar-chart" role="img" aria-label={title}></svg>
1863
</div>
@@ -23,15 +68,139 @@ const chartPayload = JSON.stringify({ data, valueFormat })
2368
.comparison-chart-container {
2469
position: relative;
2570
width: 100%;
26-
margin-top: 1em;
71+
margin-top: 0;
2772
margin-bottom: 2em;
2873
}
2974

75+
.comparison-chart-header {
76+
display: flex;
77+
align-items: center;
78+
justify-content: space-between;
79+
gap: 1em;
80+
margin-bottom: 0.75em;
81+
flex-wrap: wrap;
82+
}
83+
3084
.comparison-chart-title {
3185
font-size: 16px;
3286
font-weight: 600;
3387
color: var(--ft-text);
34-
margin: 0 0 0.75em 0;
88+
margin: 0;
89+
}
90+
91+
.sort-dropdown {
92+
position: relative;
93+
}
94+
95+
.sort-trigger {
96+
display: flex;
97+
align-items: center;
98+
gap: 5px;
99+
padding: 5px 10px;
100+
font-size: 12px;
101+
font-weight: 500;
102+
color: var(--ft-muted);
103+
background: transparent;
104+
border: 1px solid var(--ft-border);
105+
border-radius: 6px;
106+
cursor: pointer;
107+
font-family: inherit;
108+
list-style: none;
109+
user-select: none;
110+
transition:
111+
color 0.2s,
112+
border-color 0.2s;
113+
}
114+
115+
.sort-trigger::-webkit-details-marker {
116+
display: none;
117+
}
118+
119+
.sort-trigger::marker {
120+
display: none;
121+
}
122+
123+
.sort-trigger:hover {
124+
color: var(--ft-text);
125+
border-color: var(--ft-accent);
126+
}
127+
128+
.sort-trigger:focus-visible {
129+
outline: 2px solid var(--ft-accent);
130+
outline-offset: 2px;
131+
}
132+
133+
.sort-dropdown[open] .sort-trigger {
134+
color: var(--ft-accent);
135+
border-color: var(--ft-accent);
136+
}
137+
138+
.sort-icon {
139+
flex-shrink: 0;
140+
}
141+
142+
.sort-menu {
143+
position: absolute;
144+
top: calc(100% + 6px);
145+
right: 0;
146+
z-index: 50;
147+
min-width: 120px;
148+
background: var(--ft-bg);
149+
border: 1px solid var(--ft-border);
150+
border-radius: 8px;
151+
padding: 4px;
152+
margin: 0;
153+
list-style: none;
154+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
155+
opacity: 1;
156+
transform: translateY(0);
157+
transition:
158+
opacity 0.15s ease,
159+
transform 0.15s ease;
160+
}
161+
162+
@starting-style {
163+
.sort-dropdown[open] .sort-menu {
164+
opacity: 0;
165+
transform: translateY(-6px);
166+
}
167+
}
168+
169+
:global(html.dark) .sort-menu {
170+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
171+
}
172+
173+
.sort-option {
174+
display: block;
175+
width: 100%;
176+
padding: 6px 10px;
177+
font-size: 12px;
178+
font-weight: 500;
179+
color: var(--ft-muted);
180+
background: transparent;
181+
border: none;
182+
border-radius: 6px;
183+
cursor: pointer;
184+
font-family: inherit;
185+
text-align: left;
186+
transition:
187+
color 0.15s,
188+
background-color 0.15s;
189+
}
190+
191+
.sort-option:hover {
192+
color: var(--ft-text);
193+
background: var(--ft-bg-muted);
194+
}
195+
196+
.sort-option:focus-visible {
197+
outline: 2px solid var(--ft-accent);
198+
outline-offset: -2px;
199+
}
200+
201+
.sort-option.active {
202+
color: var(--ft-accent);
203+
font-weight: 600;
35204
}
36205

37206
.comparison-chart-wrapper {
@@ -118,6 +287,7 @@ const chartPayload = JSON.stringify({ data, valueFormat })
118287
HTMLElement,
119288
{ width: number; height: number }
120289
>()
290+
const chartResizeObserverCache = new WeakMap<HTMLElement, ResizeObserver>()
121291

122292
function formatValue(
123293
value: number,
@@ -144,12 +314,20 @@ const chartPayload = JSON.stringify({ data, valueFormat })
144314
tooltip.replaceChildren(titleDiv, valueDiv)
145315
}
146316

317+
function getSortedData(data: ChartDatum[], sortMode: string): ChartDatum[] {
318+
if (sortMode === 'asc') return [...data].sort((a, b) => a.value - b.value)
319+
if (sortMode === 'desc') return [...data].sort((a, b) => b.value - a.value)
320+
return data
321+
}
322+
147323
function initComparisonChart(wrapper: HTMLElement) {
148324
const svg = wrapper.querySelector('.comparison-bar-chart') as SVGSVGElement
149-
const container = wrapper.closest('.comparison-chart-container')
150-
const tooltip = container?.querySelector(
325+
const container = wrapper.closest<HTMLElement>(
326+
'.comparison-chart-container',
327+
)
328+
const tooltip = container?.querySelector<HTMLElement>(
151329
'.comparison-chart-tooltip',
152-
) as HTMLElement
330+
)
153331
if (!svg || !tooltip) return
154332

155333
const dataAttr = wrapper.dataset.comparison
@@ -167,10 +345,14 @@ const chartPayload = JSON.stringify({ data, valueFormat })
167345
const rawData = payload.data
168346
if (!rawData?.length) return
169347

170-
const data: ChartDatum[] = rawData.map((d) => ({
171-
name: String(d?.name ?? ''),
172-
value: Math.max(0, Number(d.value) || 0),
173-
}))
348+
const sortMode = container?.dataset.sortMode ?? 'default'
349+
const data: ChartDatum[] = getSortedData(
350+
rawData.map((d) => ({
351+
name: String(d?.name ?? ''),
352+
value: Math.max(0, Number(d.value) || 0),
353+
})),
354+
sortMode,
355+
)
174356

175357
const margin = { top: 20, right: 40, bottom: 80, left: 50 }
176358
const rect = wrapper.getBoundingClientRect()
@@ -322,6 +504,7 @@ const chartPayload = JSON.stringify({ data, valueFormat })
322504
chartDimensionsCache.set(wrapper, { width, height })
323505

324506
if (typeof ResizeObserver !== 'undefined') {
507+
chartResizeObserverCache.get(wrapper)?.disconnect()
325508
const ro = new ResizeObserver(() => {
326509
const r = wrapper.getBoundingClientRect()
327510
const w = r.width || 0
@@ -331,17 +514,62 @@ const chartPayload = JSON.stringify({ data, valueFormat })
331514
const lastH = last?.height ?? 0
332515
if (w > 0 && h > 0 && (w !== lastW || h !== lastH)) {
333516
ro.disconnect()
517+
chartResizeObserverCache.delete(wrapper)
334518
initComparisonChart(wrapper)
335519
}
336520
})
337521
ro.observe(wrapper)
522+
chartResizeObserverCache.set(wrapper, ro)
338523
}
339524
}
340525

526+
function initSortControls() {
527+
document
528+
.querySelectorAll<HTMLDetailsElement>('.sort-dropdown')
529+
.forEach((dropdown) => {
530+
const container = dropdown.closest<HTMLElement>(
531+
'.comparison-chart-container',
532+
)
533+
const wrapper = container?.querySelector<HTMLElement>(
534+
'.comparison-chart-wrapper',
535+
)
536+
if (!container || !wrapper) return
537+
538+
const label = dropdown.querySelector<HTMLElement>('.sort-label')
539+
const options =
540+
dropdown.querySelectorAll<HTMLButtonElement>('.sort-option')
541+
542+
options.forEach((btn) => {
543+
btn.addEventListener('click', () => {
544+
options.forEach((b) => b.classList.remove('active'))
545+
btn.classList.add('active')
546+
if (label) label.textContent = btn.textContent
547+
container.dataset.sortMode = btn.dataset.sort ?? 'default'
548+
dropdown.open = false
549+
initComparisonChart(wrapper)
550+
})
551+
})
552+
553+
// Close on outside click
554+
document.addEventListener('click', (e) => {
555+
if (!dropdown.contains(e.target as Node)) dropdown.open = false
556+
})
557+
558+
// Close on Escape
559+
dropdown.addEventListener('keydown', (e) => {
560+
if (e.key === 'Escape') {
561+
dropdown.open = false
562+
dropdown.querySelector<HTMLElement>('.sort-trigger')?.focus()
563+
}
564+
})
565+
})
566+
}
567+
341568
function init() {
342569
document.querySelectorAll('.comparison-chart-wrapper').forEach((el) => {
343570
initComparisonChart(el as HTMLElement)
344571
})
572+
initSortControls()
345573
}
346574

347575
if (document.readyState === 'loading') {

0 commit comments

Comments
 (0)