Skip to content

Commit 617d7d5

Browse files
authored
ui: Accordion widget - Add 'multi' option (#5510)
Accordion widget currently only allows one section to be open at one time (by design - this is a feature). It can be handy to be able to open and close multiple sections at once. This patch adds a 'multi' option, to enable this behavior, and also allows each section to be start open with a `defaultOpen` prop. This patch also refactors the Accordion widget to use a composite component approach: ```ts m(Accordion, m(AccordionSection, {header: 'Foo'}, 'Foo content'), m(AccordionSection, {header: 'Bar'}, 'Bar content'), ) ```
1 parent 455f5ed commit 617d7d5

6 files changed

Lines changed: 207 additions & 192 deletions

File tree

ui/src/assets/widgets/accordion.scss

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,11 @@
1515
.pf-accordion {
1616
display: flex;
1717
flex-direction: column;
18+
}
1819

19-
&__item {
20-
&.pf-expanded {
21-
.pf-accordion__header {
22-
background: var(--pf-color-background-tertiary);
23-
}
24-
}
25-
}
26-
27-
&__header {
20+
.pf-accordion__item {
21+
> summary {
22+
list-style: none;
2823
display: flex;
2924
align-items: center;
3025
gap: 4px;
@@ -38,24 +33,48 @@
3833
&:hover {
3934
background: color_hover(transparent);
4035
}
41-
}
4236

43-
&__toggle {
44-
color: var(--pf-color-text-muted);
45-
display: flex;
46-
align-items: center;
37+
// Remove the default WebKit disclosure triangle.
38+
&::-webkit-details-marker {
39+
display: none;
40+
}
41+
42+
&:focus-visible {
43+
outline: 2px solid #1a73e8;
44+
outline-offset: -2px;
45+
}
4746
}
4847

49-
&__header-content {
50-
flex: 1;
51-
min-width: 0;
52-
overflow: hidden;
53-
text-overflow: ellipsis;
54-
white-space: nowrap;
48+
&[open] > summary {
49+
background: var(--pf-color-background-tertiary);
50+
51+
.pf-accordion__toggle {
52+
transform: rotate(0deg);
53+
}
5554
}
5655

57-
&__content {
58-
padding: 8px 16px 16px 16px;
59-
background: var(--pf-color-background);
56+
&:not([open]) > summary {
57+
.pf-accordion__toggle {
58+
transform: rotate(-90deg);
59+
}
6060
}
6161
}
62+
63+
.pf-accordion__toggle {
64+
color: var(--pf-color-text-muted);
65+
display: flex;
66+
align-items: center;
67+
}
68+
69+
.pf-accordion__header-content {
70+
flex: 1;
71+
min-width: 0;
72+
overflow: hidden;
73+
text-overflow: ellipsis;
74+
white-space: nowrap;
75+
}
76+
77+
.pf-accordion__content {
78+
padding: 8px 16px 16px 16px;
79+
background: var(--pf-color-background);
80+
}

ui/src/plugins/dev.perfetto.DataExplorer/dashboard/dashboard.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
perfettoSqlTypeIcon,
2121
perfettoSqlTypeToString,
2222
} from '../../../trace_processor/perfetto_sql_type';
23-
import {Accordion, AccordionItem} from '../../../widgets/accordion';
23+
import {Accordion, AccordionSection} from '../../../widgets/accordion';
2424
import {Button} from '../../../widgets/button';
2525
import {Icons} from '../../../base/semantic_icons';
2626
import {Chip} from '../../../widgets/chip';
@@ -138,7 +138,6 @@ interface EditingChartContext {
138138

139139
export class Dashboard implements m.ClassComponent<DashboardAttrs> {
140140
private activePanel?: SidePanelTab;
141-
private expandedInput: string | undefined = undefined;
142141
private renamingChartId?: string;
143142
private editingChart?: EditingChartContext;
144143
private editPanelRenaming = false;
@@ -1325,47 +1324,37 @@ export class Dashboard implements m.ClassComponent<DashboardAttrs> {
13251324
const used = allExported.filter((s) => usedNodeIds.has(s.nodeId));
13261325
const unused = allExported.filter((s) => !usedNodeIds.has(s.nodeId));
13271326

1328-
const makeSourceItems = (srcs: DashboardDataSource[]): AccordionItem[] =>
1329-
srcs.map((source) => ({
1330-
id: source.nodeId,
1331-
header: m('.pf-dashboard__source-row', [
1332-
m('code.pf-dashboard__input-name', source.name),
1333-
]),
1334-
content: this.renderInputContent(source, attrs),
1335-
}));
1327+
const renderSourceAccordion = (srcs: DashboardDataSource[]) =>
1328+
m(
1329+
Accordion,
1330+
srcs.map((source) =>
1331+
m(
1332+
AccordionSection,
1333+
{
1334+
key: source.nodeId,
1335+
summary: m('.pf-dashboard__source-row', [
1336+
m('code.pf-dashboard__input-name', source.name),
1337+
]),
1338+
},
1339+
this.renderInputContent(source, attrs),
1340+
),
1341+
),
1342+
);
13361343

13371344
const sections: m.Child[] = [];
13381345
if (used.length > 0) {
13391346
sections.push(
13401347
m('.pf-dashboard__panel-section', [
13411348
m('.pf-dashboard__panel-section-title', 'Used data sources'),
1342-
m(
1343-
'.pf-dashboard__input-list',
1344-
m(Accordion, {
1345-
items: makeSourceItems(used),
1346-
expanded: this.expandedInput,
1347-
onToggle: (id) => {
1348-
this.expandedInput = id;
1349-
},
1350-
}),
1351-
),
1349+
m('.pf-dashboard__input-list', renderSourceAccordion(used)),
13521350
]),
13531351
);
13541352
}
13551353
if (unused.length > 0) {
13561354
sections.push(
13571355
m('.pf-dashboard__panel-section', [
13581356
m('.pf-dashboard__panel-section-title', 'Unused data sources'),
1359-
m(
1360-
'.pf-dashboard__input-list',
1361-
m(Accordion, {
1362-
items: makeSourceItems(unused),
1363-
expanded: this.expandedInput,
1364-
onToggle: (id) => {
1365-
this.expandedInput = id;
1366-
},
1367-
}),
1368-
),
1357+
m('.pf-dashboard__input-list', renderSourceAccordion(unused)),
13691358
]),
13701359
);
13711360
}

ui/src/plugins/dev.perfetto.DataExplorer/query_builder/nodes/trace_summary_node.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
} from '../node_styling_widgets';
3333
import {MetricsNode} from './metrics_node';
3434
import {TraceSummaryResultsPanel} from './trace_summary_results_panel';
35-
import {Accordion} from '../../../../widgets/accordion';
35+
import {Accordion, AccordionSection} from '../../../../widgets/accordion';
3636
import {enumKeyToLabel} from './metrics_enum_utils';
3737
import {showModal} from '../../../../widgets/modal';
3838
import {CodeSnippet} from '../../../../widgets/code_snippet';
@@ -183,13 +183,16 @@ export class TraceSummaryNode implements QueryNode {
183183
});
184184
} else {
185185
sections.push({
186-
content: m(Accordion, {
187-
items: metricsNodes.map((mn) => ({
188-
id: mn.nodeId,
189-
header: this.renderMetricHeader(mn),
190-
content: this.renderMetricContent(mn),
191-
})),
192-
}),
186+
content: m(
187+
Accordion,
188+
metricsNodes.map((mn) =>
189+
m(
190+
AccordionSection,
191+
{key: mn.nodeId, summary: this.renderMetricHeader(mn)},
192+
this.renderMetricContent(mn),
193+
),
194+
),
195+
),
193196
});
194197
}
195198

ui/src/plugins/dev.perfetto.QueryPage/table_list.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import m from 'mithril';
1616
import {FuzzyFinder, FuzzySegment} from '../../base/fuzzy';
17-
import {Accordion, AccordionItem} from '../../widgets/accordion';
17+
import {Accordion, AccordionSection} from '../../widgets/accordion';
1818
import {Button} from '../../widgets/button';
1919
import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
2020
import {Icon} from '../../widgets/icon';
@@ -45,7 +45,6 @@ export interface TableListAttrs {
4545

4646
export class TableList implements m.ClassComponent<TableListAttrs> {
4747
private searchQuery = '';
48-
private expandedTable: string | undefined = undefined;
4948

5049
view({attrs}: m.CVnode<TableListAttrs>): m.Children {
5150
const tables = attrs.sqlModules.listTables();
@@ -66,15 +65,6 @@ export class TableList implements m.ClassComponent<TableListAttrs> {
6665
}));
6766
}
6867

69-
const items: AccordionItem[] = filteredTables.map(({table, segments}) => ({
70-
id: table.name,
71-
header: m(
72-
'code.pf-simple-table-list__item-name',
73-
renderHighlightedName(segments),
74-
),
75-
content: this.renderTableContent(table, attrs.onQueryTable),
76-
}));
77-
7868
return m(
7969
'.pf-simple-table-list',
8070
m(TextInput, {
@@ -86,16 +76,25 @@ export class TableList implements m.ClassComponent<TableListAttrs> {
8676
this.searchQuery = value;
8777
},
8878
}),
89-
items.length > 0
79+
filteredTables.length > 0
9080
? m(
9181
'.pf-simple-table-list__items',
92-
m(Accordion, {
93-
items,
94-
expanded: this.expandedTable,
95-
onToggle: (id) => {
96-
this.expandedTable = id;
97-
},
98-
}),
82+
m(
83+
Accordion,
84+
filteredTables.map(({table, segments}) =>
85+
m(
86+
AccordionSection,
87+
{
88+
key: table.name,
89+
summary: m(
90+
'code.pf-simple-table-list__item-name',
91+
renderHighlightedName(segments),
92+
),
93+
},
94+
this.renderTableContent(table, attrs.onQueryTable),
95+
),
96+
),
97+
),
9998
)
10099
: m(EmptyState, {
101100
title: 'No matching tables found',

ui/src/plugins/dev.perfetto.WidgetsPage/demos/accordion_demo.ts

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,8 @@
1313
// limitations under the License.
1414

1515
import m from 'mithril';
16-
import {Accordion, AccordionItem} from '../../../widgets/accordion';
17-
18-
const DEMO_ITEMS: AccordionItem[] = [
19-
{
20-
id: 'section1',
21-
header: 'Section 1',
22-
content: m(
23-
'div',
24-
m('p', 'This is the content for section 1.'),
25-
m('p', 'The accordion ensures only one section is expanded at a time.'),
26-
),
27-
},
28-
{
29-
id: 'section2',
30-
header: 'Section 2',
31-
content: m(
32-
'div',
33-
m('p', 'Content for section 2.'),
34-
m('p', 'Click another header to collapse this and expand that one.'),
35-
),
36-
},
37-
{
38-
id: 'section3',
39-
header: 'Section 3',
40-
content: m('p', 'Content for section 3.'),
41-
},
42-
];
16+
import {Accordion, AccordionSection} from '../../../widgets/accordion';
17+
import {renderWidgetShowcase} from '../widgets_page_utils';
4318

4419
export function renderAccordion(): m.Children {
4520
return [
@@ -48,26 +23,53 @@ export function renderAccordion(): m.Children {
4823
m('h1', 'Accordion'),
4924
m(
5025
'p',
51-
'A collapsible panel component that displays a list of items where ' +
52-
'only one item can be expanded at a time. Supports both controlled ' +
53-
'and uncontrolled modes.',
26+
'A collapsible panel component. Each section uses a native ' +
27+
'<details> element and manages its own open/closed state.',
5428
),
5529
),
56-
m('h2', 'Uncontrolled Mode'),
57-
m(
58-
'p',
59-
'In uncontrolled mode, the accordion manages its own expanded state internally. ' +
60-
'All items start collapsed.',
61-
),
62-
m(
63-
'div',
64-
{
65-
style: {
66-
border: '1px solid var(--pf-color-border)',
67-
borderRadius: '4px',
68-
},
69-
},
70-
m(Accordion, {items: DEMO_ITEMS}),
71-
),
30+
31+
renderWidgetShowcase({
32+
renderWidget: ({defaultOpen, ...rest}) =>
33+
m(
34+
'div',
35+
{
36+
style: {
37+
border: '1px solid var(--pf-color-border)',
38+
borderRadius: '4px',
39+
},
40+
},
41+
m(
42+
Accordion,
43+
{key: defaultOpen ? 'open' : 'closed', ...rest},
44+
m(
45+
AccordionSection,
46+
{summary: 'Section 1', defaultOpen},
47+
m(
48+
'div',
49+
m('p', 'This is the content for section 1.'),
50+
m(
51+
'p',
52+
'Each section independently manages its open/closed state.',
53+
),
54+
),
55+
),
56+
m(
57+
AccordionSection,
58+
{summary: 'Section 2', defaultOpen},
59+
m(
60+
'div',
61+
m('p', 'Content for section 2.'),
62+
m('p', 'Multiple sections can be open simultaneously.'),
63+
),
64+
),
65+
m(
66+
AccordionSection,
67+
{summary: 'Section 3', defaultOpen},
68+
m('p', 'Content for section 3. This section starts open.'),
69+
),
70+
),
71+
),
72+
initialOpts: {multi: false, defaultOpen: false},
73+
}),
7274
];
7375
}

0 commit comments

Comments
 (0)