Skip to content

Commit 9dcc08f

Browse files
refactor(atomic): create FoldedItemListContextController for Lit migration (#6626)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexprudhomme <[email protected]>
1 parent ea03558 commit 9dcc08f

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {LitElement} from 'lit';
2+
import {customElement, state} from 'lit/decorators.js';
3+
import {beforeEach, describe, expect, it, vi} from 'vitest';
4+
import {
5+
FoldedItemListContextController,
6+
MissingParentError,
7+
} from './folded-item-list-context-controller';
8+
9+
@customElement('test-element')
10+
class TestElement extends LitElement {
11+
@state() error!: Error;
12+
13+
requestUpdate = vi.fn();
14+
}
15+
16+
describe('folded-item-list-context', () => {
17+
beforeEach(() => {
18+
vi.clearAllMocks();
19+
});
20+
21+
describe('#MissingParentError', () => {
22+
it('should create error with correct message when provided element name', () => {
23+
const error = new MissingParentError('child-element');
24+
25+
expect(error.message).toBe(
26+
'The "child-element" element must be the child of an "atomic-folded-result-list" or "atomic-insight-folded-result-list" element.'
27+
);
28+
});
29+
});
30+
31+
describe('#FoldedItemListContextController', () => {
32+
let mockElement: TestElement;
33+
let controller: FoldedItemListContextController;
34+
35+
beforeEach(() => {
36+
mockElement = new TestElement();
37+
vi.spyOn(mockElement, 'addController');
38+
vi.spyOn(mockElement, 'dispatchEvent');
39+
});
40+
41+
it('should register itself as a controller with the host', () => {
42+
controller = new FoldedItemListContextController(mockElement);
43+
44+
expect(mockElement.addController).toHaveBeenCalledWith(controller);
45+
});
46+
47+
describe('when controller is connected', () => {
48+
beforeEach(() => {
49+
controller = new FoldedItemListContextController(mockElement);
50+
});
51+
52+
it('should dispatch the resolveFoldedResultList event in #hostConnected', () => {
53+
vi.spyOn(mockElement, 'dispatchEvent').mockReturnValue(false);
54+
55+
controller.hostConnected();
56+
57+
expect(mockElement.dispatchEvent).toHaveBeenCalledWith(
58+
expect.objectContaining({
59+
type: 'atomic/resolveFoldedResultList',
60+
})
61+
);
62+
});
63+
64+
describe('when event is not canceled', () => {
65+
beforeEach(() => {
66+
vi.spyOn(mockElement, 'dispatchEvent').mockImplementation((event) => {
67+
const customEvent = event as CustomEvent;
68+
const handler = customEvent.detail;
69+
if (typeof handler === 'function') {
70+
handler({
71+
logShowMoreFoldedResults: vi.fn(),
72+
logShowLessFoldedResults: vi.fn(),
73+
});
74+
}
75+
return false;
76+
});
77+
});
78+
79+
it('should set folded item list data', () => {
80+
controller.hostConnected();
81+
82+
expect(controller.foldedItemList).toEqual({
83+
logShowMoreFoldedResults: expect.any(Function),
84+
logShowLessFoldedResults: expect.any(Function),
85+
});
86+
});
87+
88+
it('should clear error', () => {
89+
controller.hostConnected();
90+
91+
expect(controller.error).toBeNull();
92+
expect(controller.hasError).toBe(false);
93+
expect(mockElement.error).toBeUndefined();
94+
});
95+
96+
it('should request update', () => {
97+
controller.hostConnected();
98+
99+
expect(mockElement.requestUpdate).toHaveBeenCalledTimes(1);
100+
});
101+
});
102+
103+
describe('when event is canceled', () => {
104+
beforeEach(() => {
105+
vi.spyOn(mockElement, 'dispatchEvent').mockReturnValue(true);
106+
Object.defineProperty(mockElement, 'nodeName', {
107+
value: 'TEST-ELEMENT',
108+
configurable: true,
109+
});
110+
});
111+
112+
it('should set error', () => {
113+
controller.hostConnected();
114+
115+
expect(controller.error).toBeInstanceOf(MissingParentError);
116+
expect(controller.hasError).toBe(true);
117+
expect(controller.error?.message).toBe(
118+
'The "test-element" element must be the child of an "atomic-folded-result-list" or "atomic-insight-folded-result-list" element.'
119+
);
120+
expect(mockElement.error).toBe(controller.error);
121+
});
122+
123+
it('should clear folded item list data', () => {
124+
controller.hostConnected();
125+
126+
expect(controller.foldedItemList).toBeNull();
127+
});
128+
129+
it('should request update', () => {
130+
controller.hostConnected();
131+
132+
expect(mockElement.requestUpdate).toHaveBeenCalledTimes(2);
133+
});
134+
});
135+
136+
describe('#foldedItemList getter', () => {
137+
beforeEach(() => {
138+
controller = new FoldedItemListContextController(mockElement);
139+
});
140+
141+
it('should return null when there is an error', () => {
142+
vi.spyOn(mockElement, 'dispatchEvent').mockReturnValue(true);
143+
Object.defineProperty(mockElement, 'nodeName', {
144+
value: 'TEST-ELEMENT',
145+
configurable: true,
146+
});
147+
148+
controller.hostConnected();
149+
150+
expect(controller.foldedItemList).toBeNull();
151+
});
152+
153+
it('should return folded item list data when there is no error', () => {
154+
const mockFoldedItemList = {
155+
logShowMoreFoldedResults: vi.fn(),
156+
logShowLessFoldedResults: vi.fn(),
157+
};
158+
159+
vi.spyOn(mockElement, 'dispatchEvent').mockImplementation((event) => {
160+
const customEvent = event as CustomEvent;
161+
const handler = customEvent.detail;
162+
if (typeof handler === 'function') {
163+
handler(mockFoldedItemList);
164+
}
165+
return false;
166+
});
167+
168+
controller.hostConnected();
169+
170+
expect(controller.foldedItemList).toBe(mockFoldedItemList);
171+
});
172+
});
173+
});
174+
});
175+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type {ReactiveController, ReactiveControllerHost} from 'lit';
2+
import type {LitElementWithError} from '@/src/decorators/types.js';
3+
import {buildCustomEvent} from '@/src/utils/event-utils';
4+
5+
const foldedItemListContextEventName = 'atomic/resolveFoldedResultList';
6+
7+
export class MissingParentError extends Error {
8+
constructor(elementName: string) {
9+
super(
10+
`The "${elementName}" element must be the child of an "atomic-folded-result-list" or "atomic-insight-folded-result-list" element.`
11+
);
12+
}
13+
}
14+
15+
/**
16+
* A reactive controller that manages folded item list context data from parent components.
17+
* Handles fetching folded item list data via custom events and manages error states.
18+
*/
19+
export class FoldedItemListContextController<T = unknown>
20+
implements ReactiveController
21+
{
22+
private host: ReactiveControllerHost & LitElementWithError;
23+
private _foldedItemList: T | null = null;
24+
private _error: MissingParentError | null = null;
25+
26+
constructor(host: ReactiveControllerHost & LitElementWithError) {
27+
this.host = host;
28+
host.addController(this);
29+
}
30+
31+
get foldedItemList(): T | null {
32+
return this._error ? null : this._foldedItemList;
33+
}
34+
35+
get error(): MissingParentError | null {
36+
return this._error;
37+
}
38+
39+
get hasError(): boolean {
40+
return this._error !== null;
41+
}
42+
43+
hostConnected(): void {
44+
this._resolveFoldedItemListContext();
45+
}
46+
47+
private _resolveFoldedItemListContext(): void {
48+
const event = buildCustomEvent(
49+
foldedItemListContextEventName,
50+
(foldedItemList: T) => {
51+
this._foldedItemList = foldedItemList;
52+
this.host.requestUpdate();
53+
}
54+
);
55+
56+
const canceled = this.host.dispatchEvent(event);
57+
if (canceled) {
58+
const elementName = (this.host as Element).nodeName.toLowerCase();
59+
this._error = new MissingParentError(elementName);
60+
this._foldedItemList = null;
61+
this.host.error = this._error;
62+
this.host.requestUpdate();
63+
}
64+
}
65+
}
66+
67+
type FoldedItemListContextEventHandler<T> = (foldedItemList: T) => void;
68+
export type FoldedItemListContextEvent<T> = CustomEvent<
69+
FoldedItemListContextEventHandler<T>
70+
>;

packages/atomic/src/components/common/item-list/item-list-decorators.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const foldedItemListStateContextEventName = 'atomic/resolveFoldedResultList';
3939
/**
4040
* A [StencilJS property decorator](https://stenciljs.com/) to be used for elements nested within a folded item list.
4141
* This allows the Stencil component to modify the folded item list rendered levels.
42+
* @deprecated use FoldedItemListContextController instead.
4243
*/
4344
export function FoldedItemListStateContext() {
4445
return (component: ComponentInterface, foldedListState: string) => {

0 commit comments

Comments
 (0)