Skip to content

Commit 3acf724

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[throttling] Update rule list items more cleanly to preserve focus
This requires teaching ListWidget to update items in-place. Fixed: 461577228 Change-Id: Ie3aa514779c7d2844691d1f2afb635f0994c498d Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7185563 Auto-Submit: Philip Pfaffe <[email protected]> Reviewed-by: Eric Leese <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]>
1 parent 26d4a81 commit 3acf724

File tree

3 files changed

+182
-33
lines changed

3 files changed

+182
-33
lines changed

front_end/panels/network/RequestConditionsDrawer.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,12 @@ export class RequestConditionsDrawer extends UI.Widget.VBox implements
401401
const element = document.createElement('div');
402402
this.#listElements.set(condition, element);
403403
element.classList.add('blocked-url');
404+
this.updateItem(element, condition, editable, index);
405+
return element;
406+
}
407+
408+
updateItem(element: HTMLElement, condition: SDK.NetworkManager.RequestCondition, editable: boolean, index: number):
409+
void {
404410
const toggle = (e: Event): void => {
405411
if (editable) {
406412
e.consume(true);
@@ -437,9 +443,9 @@ export class RequestConditionsDrawer extends UI.Widget.VBox implements
437443
@click=${toggle}
438444
type=checkbox
439445
title=${i18nString(UIStrings.enableThrottlingToggleLabel, {PH1: constructorStringOrWildcardURL})}
440-
?checked=${enabled}
441-
?disabled=${!editable || !originalOrUpgradedURLPattern}
442-
.jslog=${VisualLogging.toggle().track({ change: true })}>
446+
.checked=${enabled}
447+
.disabled=${!editable || !originalOrUpgradedURLPattern}
448+
jslog=${VisualLogging.toggle().track({ change: true })}>
443449
<devtools-button
444450
.iconName=${'arrow-up'}
445451
.variant=${Buttons.Button.Variant.ICON}
@@ -516,15 +522,14 @@ export class RequestConditionsDrawer extends UI.Widget.VBox implements
516522
<input class=blocked-url-checkbox
517523
@click=${toggle}
518524
type=checkbox
519-
?checked=${condition.enabled}
520-
?disabled=${!editable}
521-
.jslog=${VisualLogging.toggle().track({ change: true })}>
525+
.checked=${condition.enabled}
526+
.disabled=${!editable}
527+
jslog=${VisualLogging.toggle().track({ change: true })}>
522528
<div @click=${toggle} class=blocked-url-label>${wildcardURL}</div>
523529
<devtools-widget .widgetConfig=${widgetConfig(AffectedCountWidget, {condition, drawer: this})}></devtools-widget>`,
524530
// clang-format on
525531
element);
526532
}
527-
return element;
528533
}
529534

530535
private toggleEnabled(): void {
@@ -609,19 +614,26 @@ export class RequestConditionsDrawer extends UI.Widget.VBox implements
609614

610615
update(): void {
611616
const enabled = this.manager.requestConditions.conditionsEnabled;
612-
this.list.clear();
613-
for (const pattern of this.manager.requestConditions.conditions) {
614-
if (Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled || pattern.wildcardURL) {
615-
this.list.appendItem(
616-
pattern,
617-
enabled,
618-
/* focusable=*/ false,
619-
{
620-
edit: i18nString(UIStrings.editPattern, {PH1: pattern.constructorStringOrWildcardURL}),
621-
delete: i18nString(UIStrings.removePattern, {PH1: pattern.constructorStringOrWildcardURL})
622-
},
623-
);
624-
}
617+
const newItems = Array.from(this.manager.requestConditions.conditions.filter(
618+
pattern => Root.Runtime.hostConfig.devToolsIndividualRequestThrottling?.enabled || pattern.wildcardURL));
619+
620+
let oldIndex = 0;
621+
for (; oldIndex < newItems.length; ++oldIndex) {
622+
const pattern = newItems[oldIndex];
623+
this.list.updateItem(
624+
oldIndex,
625+
pattern,
626+
enabled,
627+
/* focusable=*/ false,
628+
{
629+
edit: i18nString(UIStrings.editPattern, {PH1: pattern.constructorStringOrWildcardURL}),
630+
delete: i18nString(UIStrings.removePattern, {PH1: pattern.constructorStringOrWildcardURL})
631+
},
632+
);
633+
}
634+
635+
while (oldIndex < this.list.items.length) {
636+
this.list.removeItem(oldIndex);
625637
}
626638
this.requestUpdate();
627639
}

front_end/ui/legacy/ListWidget.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,110 @@ describeWithLocale('ListWidget', () => {
7575
list.addNewItem(0, '');
7676
assert.isTrue(delegate.committed);
7777
});
78+
79+
class MockDelegate implements UI.ListWidget.Delegate<string> {
80+
updateItem(content: Element, item: string): void {
81+
content.textContent = item;
82+
}
83+
84+
renderItem(item: string): Element {
85+
const element = document.createElement('div');
86+
element.textContent = item;
87+
return element;
88+
}
89+
90+
removeItemRequested(): void {
91+
}
92+
93+
beginEdit(): UI.ListWidget.Editor<string> {
94+
return new UI.ListWidget.Editor<string>();
95+
}
96+
97+
commitEdit(): void {
98+
}
99+
}
100+
101+
describe('updates', () => {
102+
it('an item at the beginning', () => {
103+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
104+
105+
listWidget.appendItem('a', true);
106+
listWidget.appendItem('b', true);
107+
listWidget.appendItem('c', true);
108+
109+
listWidget.updateItem(0, 'd', true);
110+
111+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
112+
assert.deepEqual(items, ['d', 'b', 'c']);
113+
});
114+
115+
it('an item in the middle', () => {
116+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
117+
118+
listWidget.appendItem('a', true);
119+
listWidget.appendItem('b', true);
120+
listWidget.appendItem('c', true);
121+
122+
listWidget.updateItem(1, 'd', true);
123+
124+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
125+
assert.deepEqual(items, ['a', 'd', 'c']);
126+
});
127+
128+
it('an item at the end', () => {
129+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
130+
131+
listWidget.appendItem('a', true);
132+
listWidget.appendItem('b', true);
133+
listWidget.appendItem('c', true);
134+
135+
listWidget.updateItem(2, 'd', true);
136+
137+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
138+
assert.deepEqual(items, ['a', 'b', 'd']);
139+
});
140+
141+
it('an item with an index < 0', () => {
142+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
143+
144+
listWidget.appendItem('a', true);
145+
listWidget.appendItem('b', true);
146+
listWidget.appendItem('c', true);
147+
148+
listWidget.updateItem(-1, 'd', true);
149+
150+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
151+
assert.deepEqual(items, ['a', 'b', 'c', 'd']);
152+
});
153+
154+
it('an item with an index > list length', () => {
155+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
156+
157+
listWidget.appendItem('a', true);
158+
listWidget.appendItem('b', true);
159+
listWidget.appendItem('c', true);
160+
161+
listWidget.updateItem(10, 'd', true);
162+
163+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
164+
assert.deepEqual(items, ['a', 'b', 'c', 'd']);
165+
166+
listWidget.element.remove();
167+
});
168+
169+
it('reorders items', () => {
170+
const listWidget = new UI.ListWidget.ListWidget(new MockDelegate());
171+
172+
listWidget.appendItem('a', true);
173+
listWidget.appendItem('b', true);
174+
listWidget.appendItem('c', true);
175+
176+
listWidget.updateItem(2, 'a', true);
177+
listWidget.updateItem(1, 'c', true);
178+
listWidget.updateItem(0, 'b', true);
179+
180+
const items = Array.from(listWidget.element.shadowRoot!.querySelectorAll('.list-item')).map(i => i.textContent);
181+
assert.deepEqual(items, ['b', 'c', 'a']);
182+
});
183+
});
78184
});

front_end/ui/legacy/ListWidget.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ export class ListWidget<T> extends VBox {
5656
private readonly list: HTMLElement;
5757
private lastSeparator: boolean;
5858
private focusRestorer: ElementFocusRestorer|null;
59-
private items: T[];
59+
#items: T[];
6060
private editable: boolean[];
61-
private elements: Element[];
61+
private elements: HTMLElement[];
6262
private editor: Editor<T>|null;
6363
private editItem: T|null;
64-
private editElement: Element|null;
64+
private editElement: HTMLElement|null;
6565
private emptyPlaceholder: Element|null;
6666
private isTable: boolean;
6767
constructor(delegate: Delegate<T>, delegatesFocus = true, isTable = false) {
@@ -73,7 +73,7 @@ export class ListWidget<T> extends VBox {
7373

7474
this.lastSeparator = false;
7575
this.focusRestorer = null;
76-
this.items = [];
76+
this.#items = [];
7777
this.editable = [];
7878
this.elements = [];
7979
this.editor = null;
@@ -90,8 +90,12 @@ export class ListWidget<T> extends VBox {
9090
this.updatePlaceholder();
9191
}
9292

93+
get items(): T[] {
94+
return this.#items;
95+
}
96+
9397
clear(): void {
94-
this.items = [];
98+
this.#items = [];
9599
this.editable = [];
96100
this.elements = [];
97101
this.lastSeparator = false;
@@ -100,8 +104,34 @@ export class ListWidget<T> extends VBox {
100104
this.stopEditing();
101105
}
102106

107+
updateItem(index: number, newItem: T, editable: boolean, focusable = true, controlLabels: {
108+
edit?: string,
109+
delete?: string,
110+
} = {}): void {
111+
if (index < 0 || index >= this.#items.length) {
112+
this.appendItem(newItem, editable, focusable, controlLabels);
113+
return;
114+
}
115+
116+
this.#items[index] = newItem;
117+
this.editable[index] = editable;
118+
const element = this.elements[index];
119+
const [content, controls] = element.children;
120+
if (controls) {
121+
element.removeChild(controls);
122+
}
123+
this.delegate.updateItem?.(content, newItem, editable, index);
124+
element.classList.toggle('editable', editable);
125+
if (editable) {
126+
if (focusable) {
127+
element.tabIndex = 0;
128+
}
129+
element.appendChild(this.createControls(newItem, element, controlLabels));
130+
}
131+
}
132+
103133
appendItem(item: T, editable: boolean, focusable = true, controlLabels: {edit?: string, delete?: string} = {}): void {
104-
if (this.lastSeparator && this.items.length) {
134+
if (this.lastSeparator && this.#items.length) {
105135
const element = document.createElement('div');
106136
element.classList.add('list-separator');
107137
if (this.isTable) {
@@ -111,14 +141,14 @@ export class ListWidget<T> extends VBox {
111141
}
112142
this.lastSeparator = false;
113143

114-
this.items.push(item);
144+
this.#items.push(item);
115145
this.editable.push(editable);
116146

117147
const element = this.list.createChild('div', 'list-item');
118148
if (this.isTable) {
119149
element.role = 'rowgroup';
120150
}
121-
const content = this.delegate.renderItem(item, editable, this.items.length - 1);
151+
const content = this.delegate.renderItem(item, editable, this.#items.length - 1);
122152
if (!content.hasAttribute('jslog')) {
123153
element.setAttribute('jslog', `${VisualLogging.item()}`);
124154
}
@@ -139,7 +169,7 @@ export class ListWidget<T> extends VBox {
139169
}
140170

141171
removeItem(index: number): void {
142-
if (this.editItem === this.items[index]) {
172+
if (this.editItem === this.#items[index]) {
143173
this.stopEditing();
144174
}
145175

@@ -160,7 +190,7 @@ export class ListWidget<T> extends VBox {
160190
element.remove();
161191

162192
this.elements.splice(index, 1);
163-
this.items.splice(index, 1);
193+
this.#items.splice(index, 1);
164194
this.editable.splice(index, 1);
165195
this.updatePlaceholder();
166196
}
@@ -174,7 +204,7 @@ export class ListWidget<T> extends VBox {
174204
this.updatePlaceholder();
175205
}
176206

177-
private createControls(item: T, element: Element, controlLabels: {edit?: string, delete?: string}): Element {
207+
private createControls(item: T, element: HTMLElement, controlLabels: {edit?: string, delete?: string}): Element {
178208
const controls = document.createElement('div');
179209
controls.classList.add('controls-container');
180210
controls.classList.add('fill');
@@ -211,7 +241,7 @@ export class ListWidget<T> extends VBox {
211241
function onRemoveClicked(this: ListWidget<T>): void {
212242
const index = this.elements.indexOf(element);
213243
this.element.focus();
214-
this.delegate.removeItemRequested(this.items[index], index);
244+
this.delegate.removeItemRequested(this.#items[index], index);
215245
ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.removedItem));
216246
if (this.elements.length >= 1) {
217247
// focus on the next item in the list, or the last item if we're removing the last item
@@ -237,7 +267,7 @@ export class ListWidget<T> extends VBox {
237267
}
238268
}
239269

240-
private startEditing(item: T, element: Element|null, insertionPoint: Element|null): void {
270+
private startEditing(item: T, element: HTMLElement|null, insertionPoint: Element|null): void {
241271
if (element && this.editElement === element) {
242272
return;
243273
}
@@ -299,6 +329,7 @@ export class ListWidget<T> extends VBox {
299329
}
300330

301331
export interface Delegate<T> {
332+
updateItem?(content: Element, newItem: T, editable: boolean, index: number): void;
302333
renderItem(item: T, editable: boolean, index: number): Element;
303334
removeItemRequested(item: T, index: number): void;
304335
beginEdit(item: T): Editor<T>;

0 commit comments

Comments
 (0)