Skip to content

Commit 845df11

Browse files
committed
refactor(aria/toolbar): use SortedCollection
1 parent bb4f8ec commit 845df11

5 files changed

Lines changed: 66 additions & 27 deletions

File tree

goldens/aria/toolbar/index.api.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import * as _angular_cdk_bidi from '@angular/cdk/bidi';
88
import * as _angular_core from '@angular/core';
99
import { OnDestroy } from '@angular/core';
1010
import { OnInit } from '@angular/core';
11+
import { Signal } from '@angular/core';
1112

1213
// @public
13-
export class Toolbar<V> {
14+
export class Toolbar<V> implements OnDestroy {
1415
constructor();
16+
readonly _collection: SortedCollection<ToolbarWidget<V>>;
1517
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1618
readonly element: HTMLElement;
1719
readonly _itemPatterns: _angular_core.Signal<ToolbarWidgetPattern<V>[]>;
20+
// (undocumented)
21+
ngOnDestroy(): void;
1822
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
1923
readonly _pattern: ToolbarPattern<V>;
20-
// (undocumented)
21-
_register(widget: ToolbarWidget<V>): void;
2224
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
2325
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
24-
// (undocumented)
25-
_unregister(widget: ToolbarWidget<V>): void;
2626
readonly value: _angular_core.ModelSignal<V[]>;
2727
readonly wrap: _angular_core.InputSignalWithTransform<boolean, unknown>;
2828
// (undocumented)

src/aria/toolbar/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ ts_project(
2626
":toolbar",
2727
"//:node_modules/@angular/core",
2828
"//:node_modules/@angular/platform-browser",
29+
"//src/aria/private/testing",
2930
"//src/cdk/testing/private",
3031
],
3132
)

src/aria/toolbar/toolbar-widget.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ export class ToolbarWidget<V> implements OnInit, OnDestroy {
107107
});
108108

109109
ngOnInit() {
110-
this._toolbar._register(this);
110+
this._toolbar._collection.register(this);
111111
}
112112

113113
ngOnDestroy() {
114-
this._toolbar._unregister(this);
114+
this._toolbar._collection.unregister(this);
115115
}
116116
}

src/aria/toolbar/toolbar.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@angular/core';
99
import {ComponentFixture, TestBed} from '@angular/core/testing';
1010
import {By} from '@angular/platform-browser';
11+
import {waitForMicrotasks} from '../private/testing/test-helpers';
1112
import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private';
1213
import {Toolbar} from './toolbar';
1314
import {ToolbarWidgetGroup} from './toolbar-widget-group';
@@ -98,6 +99,33 @@ describe('Toolbar', () => {
9899

99100
afterEach(async () => await runAccessibilityChecks(fixture.nativeElement));
100101

102+
describe('dynamic updates', () => {
103+
it('should update widget order correctly after widgets are shuffled', async () => {
104+
TestBed.configureTestingModule({imports: [ShuffledToolbarExample]});
105+
fixture = TestBed.createComponent(
106+
ShuffledToolbarExample,
107+
) as unknown as ComponentFixture<ToolbarExample>;
108+
fixture.detectChanges();
109+
const shuffledToolbarDebugEl = fixture.debugElement.query(By.directive(Toolbar));
110+
const shuffledToolbarInstance = shuffledToolbarDebugEl.injector.get(Toolbar);
111+
112+
const widgetsBefore = shuffledToolbarInstance._itemPatterns();
113+
expect(widgetsBefore.length).toBe(3);
114+
expect(widgetsBefore[0].element()?.textContent?.trim()).toBe('item 0');
115+
116+
const items = (fixture.componentInstance as unknown as ShuffledToolbarExample).items();
117+
const firstItem = items.shift()!;
118+
items.push(firstItem);
119+
(fixture.componentInstance as unknown as ShuffledToolbarExample).items.set([...items]);
120+
fixture.detectChanges();
121+
await waitForMicrotasks();
122+
123+
const widgetsAfter = shuffledToolbarInstance._itemPatterns();
124+
expect(widgetsAfter.length).toBe(3);
125+
expect(widgetsAfter[0].element()?.textContent?.trim()).toBe('item 1');
126+
});
127+
});
128+
101129
describe('Navigation', () => {
102130
describe('with horizontal orientation', () => {
103131
it('should navigate on click (horizontal)', () => {
@@ -724,3 +752,18 @@ export class SimpleToolbarButton {
724752
changeDetection: ChangeDetectionStrategy.Eager,
725753
})
726754
class WrappedToolbarExample {}
755+
756+
@Component({
757+
template: `
758+
<div ngToolbar>
759+
@for (item of items(); track item) {
760+
<button ngToolbarWidget [value]="item.value">{{item.value}}</button>
761+
}
762+
</div>
763+
`,
764+
imports: [Toolbar, ToolbarWidget],
765+
changeDetection: ChangeDetectionStrategy.Eager,
766+
})
767+
class ShuffledToolbarExample {
768+
items = signal([{value: 'item 0'}, {value: 'item 1'}, {value: 'item 2'}]);
769+
}

src/aria/toolbar/toolbar.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
*/
88

99
import {
10+
afterNextRender,
1011
afterRenderEffect,
12+
booleanAttribute,
13+
computed,
1114
Directive,
1215
ElementRef,
1316
inject,
14-
computed,
1517
input,
16-
booleanAttribute,
17-
signal,
1818
model,
19+
OnDestroy,
20+
signal,
1921
} from '@angular/core';
20-
import {ToolbarPattern, ToolbarWidgetPattern, sortDirectives} from '../private';
22+
import {ToolbarPattern, ToolbarWidgetPattern, SortedCollection} from '../private';
2123
import {Directionality} from '@angular/cdk/bidi';
2224
import type {ToolbarWidget} from './toolbar-widget';
2325

@@ -57,22 +59,22 @@ import type {ToolbarWidget} from './toolbar-widget';
5759
'(focusin)': '_pattern.onFocusIn()',
5860
},
5961
})
60-
export class Toolbar<V> {
62+
export class Toolbar<V> implements OnDestroy {
6163
/** A reference to the host element. */
6264
private readonly _elementRef = inject(ElementRef);
6365

6466
/** A reference to the host element. */
6567
readonly element = this._elementRef.nativeElement as HTMLElement;
6668

67-
/** The TabList nested inside of the container. */
68-
private readonly _widgets = signal(new Set<ToolbarWidget<V>>());
69+
/** The collection of widgets in the toolbar. */
70+
readonly _collection = new SortedCollection<ToolbarWidget<V>>();
6971

7072
/** Text direction. */
7173
readonly textDirection = inject(Directionality).valueSignal;
7274

7375
/** Sorted UIPatterns of the child widgets */
7476
readonly _itemPatterns = computed<ToolbarWidgetPattern<V>[]>(() =>
75-
[...this._widgets()].sort(sortDirectives).map(widget => widget._pattern),
77+
this._collection.orderedItems().map(widget => widget._pattern),
7678
);
7779

7880
/** Whether the toolbar is vertically or horizontally oriented. */
@@ -106,21 +108,14 @@ export class Toolbar<V> {
106108

107109
constructor() {
108110
afterRenderEffect({write: () => this._pattern.setDefaultStateEffect()});
109-
}
110111

111-
_register(widget: ToolbarWidget<V>) {
112-
const widgets = this._widgets();
113-
if (!widgets.has(widget)) {
114-
widgets.add(widget);
115-
this._widgets.set(new Set(widgets));
116-
}
112+
afterNextRender(() => {
113+
this._collection.startObserving(this.element);
114+
});
117115
}
118116

119-
_unregister(widget: ToolbarWidget<V>) {
120-
const widgets = this._widgets();
121-
if (widgets.delete(widget)) {
122-
this._widgets.set(new Set(widgets));
123-
}
117+
ngOnDestroy() {
118+
this._collection.stopObserving();
124119
}
125120

126121
/** Finds the toolbar item associated with a given element. */

0 commit comments

Comments
 (0)