Skip to content

Commit 27f777f

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Wrap TabbedPane into a <devtools-tabbed-pane> custom element.
It allows defining tabs declaratively using `<div slot="tab">` elements within the custom element. The `ProtocolMonitor` panel is updated to use the new declarative `devtools-tabbed-pane`. DOMHelpers.ts is updated to improve test cleanup by ensuring widgets are properly detached and removed from the DOM, even if they have `hideOnDetach` set. Bug: 407751340 Change-Id: I51f12c8c28fd159d2e0c0879803bda8c2c8e556f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7716258 Auto-Submit: Danil Somsikov <dsv@chromium.org> Commit-Queue: Philip Pfaffe <pfaffe@chromium.org> Commit-Queue: Danil Somsikov <dsv@chromium.org> Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
1 parent 375b941 commit 27f777f

6 files changed

Lines changed: 392 additions & 29 deletions

File tree

front_end/panels/protocol_monitor/ProtocolMonitor.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -675,28 +675,33 @@ type InfoWidgetView = (input: InfoWidgetViewInput, output: undefined, target: HT
675675

676676
const INFO_WIDGET_VIEW: InfoWidgetView = (input, _output, target) => {
677677
// clang-format off
678-
render(widget(UI.TabbedPane.TabbedPane, {
679-
tabs: [
680-
{
681-
id: 'request',
682-
title: i18nString(UIStrings.request),
683-
view: input.type === undefined ?
684-
new UI.EmptyWidget.EmptyWidget(
685-
i18nString(UIStrings.noMessageSelected), i18nString(UIStrings.selectAMessageToView)) :
686-
SourceFrame.JSONView.JSONView.createViewSync(input.request || null),
687-
enabled: input.type === 'sent',
688-
selected: input.selectedTab === 'request',
689-
},
690-
{
691-
id: 'response',
692-
title: i18nString(UIStrings.response),
693-
view: input.type === undefined ?
694-
new UI.EmptyWidget.EmptyWidget(
695-
i18nString(UIStrings.noMessageSelected), i18nString(UIStrings.selectAMessageToView)) :
696-
SourceFrame.JSONView.JSONView.createViewSync(input.response || null),
697-
selected: input.selectedTab === 'response',
698-
}
699-
]}), target);
678+
render(html`
679+
<devtools-tabbed-pane>${input.type === undefined ? html`
680+
<devtools-widget
681+
id="request" title=${i18nString(UIStrings.request)}
682+
?selected=${input.selectedTab === 'request'} disabled
683+
${widget(UI.EmptyWidget.EmptyWidget, {
684+
header: i18nString(UIStrings.noMessageSelected),
685+
text: i18nString(UIStrings.selectAMessageToView)})}>
686+
</devtools-widget>
687+
<devtools-widget
688+
id="response" title=${i18nString(UIStrings.response)}
689+
?selected=${input.selectedTab === 'response'}
690+
${widget(UI.EmptyWidget.EmptyWidget, {
691+
header: i18nString(UIStrings.noMessageSelected),
692+
text: i18nString(UIStrings.selectAMessageToView)})}>
693+
</devtools-widget>`: html`
694+
<devtools-widget
695+
id="request" title=${i18nString(UIStrings.request)}
696+
?selected=${input.selectedTab === 'request'} ?disabled=${input.type !== 'sent'}
697+
${widget(SourceFrame.JSONView.SearchableJsonView, {jsonObject: input.request})}>
698+
</devtools-widget>
699+
<devtools-widget
700+
id="response" title=${i18nString(UIStrings.response)}
701+
?selected=${input.selectedTab === 'response'}
702+
${widget(SourceFrame.JSONView.SearchableJsonView, {jsonObject: input.response})}>
703+
</devtools-widget>`}
704+
</devtools-tabbed-pane>`, target);
700705
// clang-format on
701706
};
702707

front_end/testing/DOMHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function removeChildren(node: Node): void {
6161
const widget = UI.Widget.Widget.get(firstChild);
6262
if (widget) {
6363
// Child is a widget, so we have to use the Widget system to remove it from the DOM.
64-
widget.detach();
64+
widget.detach(/* overrideHideOnDetach= */ true);
6565
continue;
6666
}
6767
// For regular children, recursively remove their children, since some of them

front_end/ui/legacy/TabbedPane.test.ts

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import {doubleRaf, raf, renderElementIntoDOM} from '../../testing/DOMHelpers.js';
66
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
7+
import {html, render} from '../../ui/lit/lit.js';
78

89
import * as UI from './legacy.js';
910

@@ -99,3 +100,240 @@ describeWithEnvironment('TabbedPane', () => {
99100
assert.strictEqual(getFocusedElementText(), 'Widget 0', 'Focus should move to Widget 0');
100101
});
101102
});
103+
104+
describeWithEnvironment('TabbedPaneElement', () => {
105+
it('creates tabs from slot elements', async () => {
106+
const container = document.createElement('div');
107+
renderElementIntoDOM(container);
108+
render(
109+
html`
110+
<devtools-tabbed-pane>
111+
<div id="tab1" title="Tab 1">Content 1</div>
112+
<div id="tab2" title="Tab 2">Content 2</div>
113+
</devtools-tabbed-pane>
114+
`,
115+
container);
116+
117+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
118+
assert.isNotNull(tabbedPaneElement);
119+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
120+
assert.isNotNull(widget);
121+
122+
await raf(); // Wait for slotchange and updateTabs
123+
124+
assert.lengthOf(widget.tabs, 2);
125+
assert.strictEqual(widget.tabs[0].id, 'tab1');
126+
assert.strictEqual(widget.tabs[0].title, 'Tab 1');
127+
assert.strictEqual(widget.tabs[1].id, 'tab2');
128+
assert.strictEqual(widget.tabs[1].title, 'Tab 2');
129+
});
130+
131+
it('creates tabs with selected and disabled attributes', async () => {
132+
const container = document.createElement('div');
133+
renderElementIntoDOM(container);
134+
render(
135+
html`
136+
<devtools-tabbed-pane>
137+
<div id="tab1" title="Tab 1" disabled>Content 1</div>
138+
<div id="tab2" title="Tab 2" selected>Content 2</div>
139+
</devtools-tabbed-pane>
140+
`,
141+
container);
142+
143+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
144+
assert.isNotNull(tabbedPaneElement);
145+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
146+
assert.isNotNull(widget);
147+
148+
await raf(); // Wait for slotchange and updateTabs
149+
150+
assert.lengthOf(widget.tabs, 2);
151+
assert.isFalse(widget.tabIsEnabled('tab1'));
152+
assert.isTrue(widget.tabIsEnabled('tab2'));
153+
assert.strictEqual(widget.selectedTabId, 'tab2');
154+
});
155+
156+
it('creates tabs with jslogcontext', async () => {
157+
const container = document.createElement('div');
158+
renderElementIntoDOM(container);
159+
render(
160+
html`
161+
<devtools-tabbed-pane>
162+
<div id="tab1" title="Tab 1" jslogcontext="log1">Content 1</div>
163+
</devtools-tabbed-pane>
164+
`,
165+
container);
166+
167+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
168+
assert.isNotNull(tabbedPaneElement);
169+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
170+
assert.isNotNull(widget);
171+
172+
await raf();
173+
174+
assert.strictEqual(widget.tabs[0].jslogContext, 'log1');
175+
});
176+
177+
it('updates tabs when attributes change', async () => {
178+
const container = document.createElement('div');
179+
renderElementIntoDOM(container);
180+
render(
181+
html`
182+
<devtools-tabbed-pane>
183+
<div id="tab1" title="Tab 1">Content 1</div>
184+
</devtools-tabbed-pane>
185+
`,
186+
container);
187+
188+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
189+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
190+
await raf();
191+
192+
const tabElement = container.querySelector('#tab1') as HTMLElement;
193+
tabElement.setAttribute('title', 'Updated Tab 1');
194+
195+
// MutationObserver needs a tick
196+
await new Promise(resolve => setTimeout(resolve, 0));
197+
198+
assert.strictEqual(widget.tabs[0].title, 'Updated Tab 1');
199+
});
200+
201+
it('updates tabs when jslogcontext attribute changes', async () => {
202+
const container = document.createElement('div');
203+
renderElementIntoDOM(container);
204+
render(
205+
html`
206+
<devtools-tabbed-pane>
207+
<div id="tab1" title="Tab 1">Content 1</div>
208+
</devtools-tabbed-pane>
209+
`,
210+
container);
211+
212+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
213+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
214+
await raf();
215+
216+
const tabElement = container.querySelector('#tab1') as HTMLElement;
217+
tabElement.setAttribute('jslogcontext', 'updated-log');
218+
219+
// MutationObserver needs a tick
220+
await new Promise(resolve => setTimeout(resolve, 0));
221+
222+
assert.strictEqual(widget.tabs[0].jslogContext, 'updated-log');
223+
});
224+
225+
it('updates tabs when selected attribute changes', async () => {
226+
const container = document.createElement('div');
227+
renderElementIntoDOM(container);
228+
render(
229+
html`
230+
<devtools-tabbed-pane>
231+
<div id="tab1" title="Tab 1">Content 1</div>
232+
<div id="tab2" title="Tab 2">Content 2</div>
233+
</devtools-tabbed-pane>
234+
`,
235+
container);
236+
237+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
238+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
239+
await raf();
240+
241+
assert.strictEqual(widget.selectedTabId, 'tab1');
242+
243+
const tab2 = container.querySelector('#tab2') as HTMLElement;
244+
tab2.setAttribute('selected', '');
245+
246+
// MutationObserver needs a tick
247+
await new Promise(resolve => setTimeout(resolve, 0));
248+
249+
assert.strictEqual(widget.selectedTabId, 'tab2');
250+
});
251+
252+
it('updates tabs when disabled attribute changes', async () => {
253+
const container = document.createElement('div');
254+
renderElementIntoDOM(container);
255+
render(
256+
html`
257+
<devtools-tabbed-pane>
258+
<div id="tab1" title="Tab 1">Content 1</div>
259+
<div id="tab2" title="Tab 2">Content 2</div>
260+
</devtools-tabbed-pane>
261+
`,
262+
container);
263+
264+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
265+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
266+
await raf();
267+
268+
assert.isTrue(widget.tabIsEnabled('tab2'));
269+
270+
const tab2 = container.querySelector('#tab2') as HTMLElement;
271+
tab2.setAttribute('disabled', '');
272+
273+
// MutationObserver needs a tick
274+
await new Promise(resolve => setTimeout(resolve, 0));
275+
276+
assert.isFalse(widget.tabIsEnabled('tab2'));
277+
});
278+
279+
it('updates tabs when children are added or removed', async () => {
280+
const container = document.createElement('div');
281+
renderElementIntoDOM(container);
282+
render(
283+
html`
284+
<devtools-tabbed-pane>
285+
<div id="tab1" title="Tab 1">Content 1</div>
286+
</devtools-tabbed-pane>
287+
`,
288+
container);
289+
290+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
291+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
292+
await raf();
293+
294+
assert.lengthOf(widget.tabs, 1);
295+
296+
// Add a new tab
297+
const newTab = document.createElement('div');
298+
newTab.id = 'tab2';
299+
newTab.setAttribute('title', 'Tab 2');
300+
tabbedPaneElement.appendChild(newTab);
301+
302+
await raf(); // wait for slotchange
303+
304+
assert.lengthOf(widget.tabs, 2);
305+
assert.strictEqual(widget.tabs[1].id, 'tab2');
306+
307+
// Remove the first tab
308+
const tab1 = container.querySelector('#tab1') as HTMLElement;
309+
tab1.remove();
310+
311+
await raf();
312+
313+
assert.lengthOf(widget.tabs, 1);
314+
assert.strictEqual(widget.tabs[0].id, 'tab2');
315+
});
316+
317+
it('supports left and right toolbars via slots', async () => {
318+
const container = document.createElement('div');
319+
renderElementIntoDOM(container);
320+
render(
321+
html`
322+
<devtools-tabbed-pane>
323+
<devtools-toolbar slot="left" id="left-toolbar"></devtools-toolbar>
324+
<devtools-toolbar slot="right" id="right-toolbar"></devtools-toolbar>
325+
</devtools-tabbed-pane>
326+
`,
327+
container);
328+
329+
const tabbedPaneElement = container.querySelector('devtools-tabbed-pane') as UI.TabbedPane.TabbedPaneElement;
330+
const widget = UI.Widget.Widget.get(tabbedPaneElement) as UI.TabbedPane.TabbedPane;
331+
await raf();
332+
333+
const leftToolbar = container.querySelector('#left-toolbar');
334+
const rightToolbar = container.querySelector('#right-toolbar');
335+
336+
assert.strictEqual(widget.leftToolbar(), leftToolbar);
337+
assert.strictEqual(widget.rightToolbar(), rightToolbar);
338+
});
339+
});

0 commit comments

Comments
 (0)