Skip to content

Commit 8150f83

Browse files
committed
refactor: Adopted root styles logic
This commit addresses several scenarios where the old implementation would fail: 1. A runtime change of the global theme. Since the theme controller overwrites the adopted stylesheets collection this would nuke previous root styles. 2. SPA router navigation where the chat is created before a certain framework passes down the 'options' object.
1 parent 9c540f4 commit 8150f83

File tree

4 files changed

+143
-56
lines changed

4 files changed

+143
-56
lines changed

src/components/chat/chat-input.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { consume } from '@lit/context';
1+
import { ContextConsumer, consume } from '@lit/context';
22
import { html, LitElement, nothing } from 'lit';
33
import { query, state } from 'lit/decorators.js';
44
import { cache } from 'lit/directives/cache.js';
@@ -24,7 +24,12 @@ import type {
2424
ChatTemplateRenderer,
2525
IgcChatMessageAttachment,
2626
} from './types.js';
27-
import { adoptPageStyles, getChatAcceptedFiles, getIconName } from './utils.js';
27+
import {
28+
addAdoptedStylesController,
29+
type ChatAcceptedFileTypes,
30+
getChatAcceptedFiles,
31+
getIconName,
32+
} from './utils.js';
2833

2934
type DefaultInputRenderers = {
3035
input: ChatTemplateRenderer<ChatInputRenderContext>;
@@ -95,8 +100,20 @@ export default class IgcChatInputComponent extends LitElement {
95100
private _userLastTypeTime = Date.now();
96101
private _typingTimeout = 0;
97102

98-
@consume({ context: chatContext, subscribe: true })
99-
private readonly _state!: ChatState;
103+
private readonly _adoptedStyles = addAdoptedStylesController(this);
104+
105+
private readonly _stateChanged = () => {
106+
this._adoptedStyles.shouldAdoptStyles(
107+
!!this._state.options?.adoptRootStyles &&
108+
!this._adoptedStyles.hasAdoptedStyles
109+
);
110+
};
111+
112+
private readonly _stateConsumer = new ContextConsumer(this, {
113+
context: chatContext,
114+
callback: this._stateChanged,
115+
subscribe: true,
116+
});
100117

101118
@consume({ context: chatUserInputContext, subscribe: true })
102119
private readonly _userInputState!: ChatState;
@@ -110,7 +127,11 @@ export default class IgcChatInputComponent extends LitElement {
110127
@state()
111128
private _parts = { 'input-container': true, dragging: false };
112129

113-
private get _acceptedTypes() {
130+
private get _state(): ChatState {
131+
return this._stateConsumer.value!;
132+
}
133+
134+
private get _acceptedTypes(): ChatAcceptedFileTypes | null {
114135
return this._state.acceptedFileTypes;
115136
}
116137

@@ -125,9 +146,7 @@ export default class IgcChatInputComponent extends LitElement {
125146
}
126147

127148
private _adoptPageStyles(): void {
128-
if (this._state.options?.adoptRootStyles) {
129-
adoptPageStyles(this);
130-
}
149+
this._adoptedStyles.shouldAdoptStyles(this._adoptedStyles.hasAdoptedStyles);
131150
}
132151

133152
private _getRenderer<U extends keyof DefaultInputRenderers>(

src/components/chat/chat-message.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { consume } from '@lit/context';
1+
import { ContextConsumer } from '@lit/context';
22
import { html, LitElement, nothing } from 'lit';
33
import { property } from 'lit/decorators.js';
44
import { cache } from 'lit/directives/cache.js';
@@ -19,7 +19,7 @@ import type {
1919
ChatTemplateRenderer,
2020
IgcChatMessage,
2121
} from './types.js';
22-
import { adoptPageStyles } from './utils.js';
22+
import { addAdoptedStylesController } from './utils.js';
2323

2424
const LIKE_INACTIVE = 'thumb_up_inactive';
2525
const LIKE_ACTIVE = 'thumb_up_active';
@@ -66,15 +66,31 @@ export default class IgcChatMessageComponent extends LitElement {
6666
);
6767
}
6868

69+
private readonly _adoptedStyles = addAdoptedStylesController(this);
70+
6971
private readonly _defaults = Object.freeze<DefaultMessageRenderers>({
7072
messageHeader: () => this._renderHeader(),
7173
messageContent: () => this._renderContent(),
7274
messageAttachments: () => this._renderAttachments(),
7375
messageActions: () => this._renderActions(),
7476
});
7577

76-
@consume({ context: chatContext, subscribe: true })
77-
private readonly _state!: ChatState;
78+
private readonly _stateChanged = () => {
79+
this._adoptedStyles.shouldAdoptStyles(
80+
!!this._state.options?.adoptRootStyles &&
81+
!this._adoptedStyles.hasAdoptedStyles
82+
);
83+
};
84+
85+
private readonly _stateConsumer = new ContextConsumer(this, {
86+
context: chatContext,
87+
callback: this._stateChanged,
88+
subscribe: true,
89+
});
90+
91+
private get _state(): ChatState {
92+
return this._stateConsumer.value!;
93+
}
7894

7995
/**
8096
* The chat message to render.
@@ -88,9 +104,7 @@ export default class IgcChatMessageComponent extends LitElement {
88104
}
89105

90106
private _adoptPageStyles(): void {
91-
if (this._state.options?.adoptRootStyles) {
92-
adoptPageStyles(this);
93-
}
107+
this._adoptedStyles.shouldAdoptStyles(this._adoptedStyles.hasAdoptedStyles);
94108
}
95109

96110
private _getRenderer(name: keyof DefaultMessageRenderers) {

src/components/chat/chat.spec.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
ChatMessageRenderContext,
2828
IgcChatMessage,
2929
IgcChatMessageAttachment,
30+
IgcChatOptions,
3031
} from './types.js';
3132

3233
describe('Chat', () => {
@@ -1031,12 +1032,20 @@ describe('Chat', () => {
10311032
});
10321033

10331034
describe('adoptRootStyles behavior', () => {
1034-
const messages: IgcChatMessage[] = [
1035-
{ id: 'id', sender: 'bot', text: 'Hello' },
1036-
];
1035+
let chat: IgcChatComponent;
1036+
10371037
const renderer = ({ message }: ChatMessageRenderContext) =>
10381038
html`<div class="custom-background">${message.text}</div>`;
10391039

1040+
async function createAdoptedStylesChat(options: IgcChatOptions) {
1041+
chat = await fixture(html`
1042+
<igc-chat
1043+
.messages=${[{ id: 'id', sender: 'bot', text: 'Hello' }]}
1044+
.options=${{ renderers: { messageContent: renderer }, ...options }}
1045+
></igc-chat>
1046+
`);
1047+
}
1048+
10401049
function verifyCustomStyles(state: boolean) {
10411050
const { messages } = getChatDOM(chat);
10421051
const { backgroundColor } = getComputedStyle(
@@ -1060,42 +1069,23 @@ describe('Chat', () => {
10601069
});
10611070

10621071
afterEach(() => {
1063-
// Reset the theme and clean the style tag
1064-
configureTheme('bootstrap');
10651072
document.head.querySelector('#adopt-styles-test')?.remove();
10661073
});
10671074

10681075
it('correctly applies `adoptRootStyles` when set', async () => {
1069-
chat.options = {
1070-
adoptRootStyles: true,
1071-
renderers: { messageContent: renderer },
1072-
};
1073-
1074-
chat.messages = messages;
1075-
1076+
await createAdoptedStylesChat({ adoptRootStyles: true });
10761077
await elementUpdated(chat);
10771078
verifyCustomStyles(true);
10781079
});
10791080

10801081
it('skips `adoptRootStyles` when not set', async () => {
1081-
chat.options = {
1082-
renderers: { messageContent: renderer },
1083-
};
1084-
1085-
chat.messages = messages;
1086-
1082+
await createAdoptedStylesChat({ adoptRootStyles: false });
10871083
await elementUpdated(chat);
10881084
verifyCustomStyles(false);
10891085
});
10901086

10911087
it('correctly reapplies `adoptRootStyles` when set and the theme is changed', async () => {
1092-
chat.options = {
1093-
adoptRootStyles: true,
1094-
renderers: { messageContent: renderer },
1095-
};
1096-
1097-
chat.messages = messages;
1098-
1088+
await createAdoptedStylesChat({ adoptRootStyles: true });
10991089
await elementUpdated(chat);
11001090
verifyCustomStyles(true);
11011091

@@ -1104,6 +1094,10 @@ describe('Chat', () => {
11041094

11051095
await elementUpdated(chat);
11061096
verifyCustomStyles(true);
1097+
1098+
configureTheme('bootstrap');
1099+
await elementUpdated(chat);
1100+
verifyCustomStyles(true);
11071101
});
11081102
});
11091103
});

src/components/chat/utils.ts

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { adoptStyles, type LitElement } from 'lit';
1+
import {
2+
adoptStyles,
3+
type LitElement,
4+
type ReactiveController,
5+
type ReactiveControllerHost,
6+
} from 'lit';
27
import { last } from '../common/util.js';
38
import type { IgcChatMessageAttachment } from './types.js';
49

@@ -109,23 +114,78 @@ export function isImageAttachment(
109114
);
110115
}
111116

112-
export function adoptPageStyles(element: LitElement): void {
113-
const sheets: CSSStyleSheet[] = [];
117+
// export function adoptPageStyles(element: LitElement): void {
118+
// const sheets: CSSStyleSheet[] = [];
119+
120+
// for (const sheet of document.styleSheets) {
121+
// try {
122+
// const constructed = new CSSStyleSheet();
123+
// for (const rule of sheet.cssRules) {
124+
// // https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule:~:text=If%20parsed%20rule%20is%20an%20%40import%20rule
125+
// if (rule.cssText.startsWith('@import')) {
126+
// continue;
127+
// }
128+
// constructed.insertRule(rule.cssText);
129+
// }
130+
// sheets.push(constructed);
131+
// } catch {}
132+
// }
133+
134+
// const ctor = element.constructor as typeof LitElement;
135+
// adoptStyles(element.shadowRoot!, [...ctor.elementStyles, ...sheets]);
136+
// }
137+
138+
class AdoptedStylesController implements ReactiveController {
139+
private readonly _host: ReactiveControllerHost & LitElement;
140+
private _hasAdoptedStyles = false;
141+
142+
public get hasAdoptedStyles(): boolean {
143+
return this._hasAdoptedStyles;
144+
}
114145

115-
for (const sheet of document.styleSheets) {
116-
try {
117-
const constructed = new CSSStyleSheet();
118-
for (const rule of sheet.cssRules) {
119-
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule:~:text=If%20parsed%20rule%20is%20an%20%40import%20rule
120-
if (rule.cssText.startsWith('@import')) {
121-
continue;
146+
private _adoptRootStyles(): void {
147+
const sheets: CSSStyleSheet[] = [];
148+
149+
for (const sheet of document.styleSheets) {
150+
try {
151+
const constructed = new CSSStyleSheet();
152+
for (const rule of sheet.cssRules) {
153+
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule:~:text=If%20parsed%20rule%20is%20an%20%40import%20rule
154+
if (rule.cssText.startsWith('@import')) {
155+
continue;
156+
}
157+
constructed.insertRule(rule.cssText);
122158
}
123-
constructed.insertRule(rule.cssText);
124-
}
125-
sheets.push(constructed);
126-
} catch {}
159+
sheets.push(constructed);
160+
} catch {}
161+
}
162+
163+
const ctor = this._host.constructor as typeof LitElement;
164+
adoptStyles(this._host.shadowRoot!, [...ctor.elementStyles, ...sheets]);
165+
}
166+
167+
constructor(host: ReactiveControllerHost & LitElement) {
168+
this._host = host;
169+
host.addController(this);
170+
}
171+
172+
public shouldAdoptStyles(condition: boolean): void {
173+
if (condition) {
174+
this._adoptRootStyles();
175+
this._hasAdoptedStyles = true;
176+
}
127177
}
128178

129-
const ctor = element.constructor as typeof LitElement;
130-
adoptStyles(element.shadowRoot!, [...ctor.elementStyles, ...sheets]);
179+
/** @internal */
180+
public hostDisconnected(): void {
181+
this._hasAdoptedStyles = false;
182+
}
131183
}
184+
185+
export function addAdoptedStylesController(
186+
host: ReactiveControllerHost & LitElement
187+
): AdoptedStylesController {
188+
return new AdoptedStylesController(host);
189+
}
190+
191+
export type { AdoptedStylesController };

0 commit comments

Comments
 (0)