Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions src/components/chat/chat-input.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ContextConsumer, consume } from '@lit/context';
import { html, LitElement, nothing } from 'lit';
import { html, LitElement, nothing, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
import { cache } from 'lit/directives/cache.js';
import { ifDefined } from 'lit/directives/if-defined.js';
Expand All @@ -8,6 +8,7 @@ import { addThemingController } from '../../theming/theming-controller.js';
import IgcIconButtonComponent from '../button/icon-button.js';
import IgcChipComponent from '../chip/chip.js';
import { chatContext, chatUserInputContext } from '../common/context.js';
import { addAdoptedStylesController } from '../common/controllers/adopt-styles.js';
import { enterKey, tabKey } from '../common/controllers/key-bindings.js';
import { registerComponent } from '../common/definitions/register.js';
import { partMap } from '../common/part-map.js';
Expand All @@ -25,7 +26,6 @@ import type {
IgcChatMessageAttachment,
} from './types.js';
import {
addAdoptedStylesController,
type ChatAcceptedFileTypes,
getChatAcceptedFiles,
getIconName,
Expand Down Expand Up @@ -103,10 +103,7 @@ export default class IgcChatInputComponent extends LitElement {
private readonly _adoptedStyles = addAdoptedStylesController(this);

private readonly _stateChanged = () => {
this._adoptedStyles.shouldAdoptStyles(
!!this._state.options?.adoptRootStyles &&
!this._adoptedStyles.hasAdoptedStyles
);
this._shouldAdoptRootStyles = Boolean(this._state.options?.adoptRootStyles);
};

private readonly _stateConsumer = new ContextConsumer(this, {
Expand All @@ -127,6 +124,9 @@ export default class IgcChatInputComponent extends LitElement {
@state()
private _parts = { 'input-container': true, dragging: false };

@state()
private _shouldAdoptRootStyles = false;

private get _state(): ChatState {
return this._stateConsumer.value!;
}
Expand All @@ -140,13 +140,21 @@ export default class IgcChatInputComponent extends LitElement {
addThemingController(this, all, { themeChange: this._adoptPageStyles });
}

protected override update(props: PropertyValues): void {
if (props.has('_shouldAdoptRootStyles')) {
this._adoptedStyles.shouldAdoptStyles(this._shouldAdoptRootStyles);
}
super.update(props);
}

/** @internal */
public focusInput(): void {
this._textInputElement?.focus();
}

private _adoptPageStyles(): void {
this._adoptedStyles.shouldAdoptStyles(this._adoptedStyles.hasAdoptedStyles);
this._adoptedStyles.invalidateCache(this.ownerDocument);
this._adoptedStyles.shouldAdoptStyles(this._shouldAdoptRootStyles);
}

private _getRenderer<U extends keyof DefaultInputRenderers>(
Expand Down
24 changes: 16 additions & 8 deletions src/components/chat/chat-message.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ContextConsumer } from '@lit/context';
import { html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { html, LitElement, nothing, type PropertyValues } from 'lit';
import { property, state } from 'lit/decorators.js';
import { cache } from 'lit/directives/cache.js';
import { until } from 'lit/directives/until.js';
import { addThemingController } from '../../theming/theming-controller.js';
import IgcIconButtonComponent from '../button/icon-button.js';
import { chatContext } from '../common/context.js';
import { addAdoptedStylesController } from '../common/controllers/adopt-styles.js';
import { registerComponent } from '../common/definitions/register.js';
import { partMap } from '../common/part-map.js';
import { isEmpty, trimmedHtml } from '../common/util.js';
Expand All @@ -19,7 +20,6 @@ import type {
ChatTemplateRenderer,
IgcChatMessage,
} from './types.js';
import { addAdoptedStylesController } from './utils.js';

const LIKE_INACTIVE = 'thumb_up_inactive';
const LIKE_ACTIVE = 'thumb_up_active';
Expand Down Expand Up @@ -76,10 +76,7 @@ export default class IgcChatMessageComponent extends LitElement {
});

private readonly _stateChanged = () => {
this._adoptedStyles.shouldAdoptStyles(
!!this._state.options?.adoptRootStyles &&
!this._adoptedStyles.hasAdoptedStyles
);
this._shouldAdoptRootStyles = Boolean(this._state.options?.adoptRootStyles);
};

private readonly _stateConsumer = new ContextConsumer(this, {
Expand All @@ -88,6 +85,9 @@ export default class IgcChatMessageComponent extends LitElement {
subscribe: true,
});

@state()
private _shouldAdoptRootStyles = false;

private get _state(): ChatState {
return this._stateConsumer.value!;
}
Expand All @@ -103,8 +103,16 @@ export default class IgcChatMessageComponent extends LitElement {
addThemingController(this, all, { themeChange: this._adoptPageStyles });
}

protected override update(props: PropertyValues): void {
if (props.has('_shouldAdoptRootStyles')) {
this._adoptedStyles.shouldAdoptStyles(this._shouldAdoptRootStyles);
}
super.update(props);
}

private _adoptPageStyles(): void {
this._adoptedStyles.shouldAdoptStyles(this._adoptedStyles.hasAdoptedStyles);
this._adoptedStyles.invalidateCache(this.ownerDocument);
this._adoptedStyles.shouldAdoptStyles(this._shouldAdoptRootStyles);
}

private _getRenderer(name: keyof DefaultMessageRenderers) {
Expand Down
53 changes: 53 additions & 0 deletions src/components/chat/chat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ import type {
describe('Chat', () => {
before(() => {
defineComponents(IgcChatComponent, IgcInputComponent);

// Suppress ResizeObserver loop errors that can occur during tests from
// the underlying igc-textarea component. These errors do not affect the tests and are not actionable.
const errorHandler = window.onerror;
window.onerror = (message, ...args) => {
if (typeof message === 'string' && message.match(/ResizeObserver loop/)) {
return true;
}
return errorHandler ? errorHandler(message, ...args) : false;
};
});

const textInputTemplate = (text: string) => html`
Expand Down Expand Up @@ -1099,6 +1109,49 @@ describe('Chat', () => {
await elementUpdated(chat);
verifyCustomStyles(true);
});

it('correctly adopts styles when toggling from false to true', async () => {
await createAdoptedStylesChat({ adoptRootStyles: false });
await elementUpdated(chat);
verifyCustomStyles(false);

// Toggle to true
chat.options = { ...chat.options, adoptRootStyles: true };
await elementUpdated(chat);
verifyCustomStyles(true);
});

it('correctly removes adopted styles when toggling from true to false', async () => {
await createAdoptedStylesChat({ adoptRootStyles: true });
await elementUpdated(chat);
verifyCustomStyles(true);

// Toggle to false
chat.options = { ...chat.options, adoptRootStyles: false };
await elementUpdated(chat);
verifyCustomStyles(false);
});

it('correctly handles multiple toggles of adoptRootStyles', async () => {
await createAdoptedStylesChat({ adoptRootStyles: false });
await elementUpdated(chat);
verifyCustomStyles(false);

// Toggle to true
chat.options = { ...chat.options, adoptRootStyles: true };
await elementUpdated(chat);
verifyCustomStyles(true);

// Toggle back to false
chat.options = { ...chat.options, adoptRootStyles: false };
await elementUpdated(chat);
verifyCustomStyles(false);

// Toggle to true again
chat.options = { ...chat.options, adoptRootStyles: true };
await elementUpdated(chat);
verifyCustomStyles(true);
});
});
});

Expand Down
2 changes: 0 additions & 2 deletions src/components/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,6 @@ export type IgcChatOptions = {
* global styles unexpectedly bleed into the component, breaking encapsulation and causing
* unpredictable visual issues.
*
* **WARNING**: This is a once time shot. Changing this property in runtime won't reflect
* its value.
*/
adoptRootStyles?: boolean;

Expand Down
61 changes: 0 additions & 61 deletions src/components/chat/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
import {
adoptStyles,
type LitElement,
type ReactiveController,
type ReactiveControllerHost,
} from 'lit';
import { last } from '../common/util.js';
import type { IgcChatMessageAttachment } from './types.js';

Expand Down Expand Up @@ -113,58 +107,3 @@ export function isImageAttachment(
attachment.type === 'image' || attachment.file?.type.startsWith('image/')
);
}

class AdoptedStylesController implements ReactiveController {
private readonly _host: ReactiveControllerHost & LitElement;
private _hasAdoptedStyles = false;

public get hasAdoptedStyles(): boolean {
return this._hasAdoptedStyles;
}

private _adoptRootStyles(): void {
const sheets: CSSStyleSheet[] = [];

for (const sheet of document.styleSheets) {
try {
const constructed = new CSSStyleSheet();
for (const rule of sheet.cssRules) {
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule:~:text=If%20parsed%20rule%20is%20an%20%40import%20rule
if (rule.cssText.startsWith('@import')) {
continue;
}
constructed.insertRule(rule.cssText);
}
sheets.push(constructed);
} catch {}
}

const ctor = this._host.constructor as typeof LitElement;
adoptStyles(this._host.shadowRoot!, [...ctor.elementStyles, ...sheets]);
}

constructor(host: ReactiveControllerHost & LitElement) {
this._host = host;
host.addController(this);
}

public shouldAdoptStyles(condition: boolean): void {
if (condition) {
this._adoptRootStyles();
this._hasAdoptedStyles = true;
}
}

/** @internal */
public hostDisconnected(): void {
this._hasAdoptedStyles = false;
}
}

export function addAdoptedStylesController(
host: ReactiveControllerHost & LitElement
): AdoptedStylesController {
return new AdoptedStylesController(host);
}

export type { AdoptedStylesController };
Loading