Skip to content

Commit c4ceae6

Browse files
ethanwintersannawen1
authored andcommitted
feat: add onViewPreChange callback (#658)
1 parent 9943c80 commit c4ceae6

File tree

11 files changed

+421
-149
lines changed

11 files changed

+421
-149
lines changed

demo/src/framework/demo-body.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,32 @@ export class DemoBody extends LitElement {
184184
this.hasReceivedSetChatConfig = setChatConfigState.hasReceivedSetChatConfig;
185185
}
186186

187+
/**
188+
* Notify any listener in parent components that settings have changed.
189+
*/
190+
private _dispatchSettingsChangeEvent() {
191+
this.dispatchEvent(
192+
new CustomEvent("demo-settings-changed", {
193+
detail: { settings: this.settings },
194+
bubbles: true,
195+
composed: true,
196+
}),
197+
);
198+
}
199+
200+
/**
201+
* Notify any listener in parent components that the chat instance reference changed.
202+
*/
203+
private _dispatchChatInstanceChangeEvent() {
204+
this.dispatchEvent(
205+
new CustomEvent("demo-chat-instance-changed", {
206+
detail: { chatInstance: this.chatInstance },
207+
bubbles: true,
208+
composed: true,
209+
}),
210+
);
211+
}
212+
187213
/**
188214
* Set the chat instance and expose it globally for tests and debugging
189215
*/
@@ -196,6 +222,7 @@ export class DemoBody extends LitElement {
196222
delete window.chatInstance;
197223
}
198224

225+
this._dispatchChatInstanceChangeEvent();
199226
this.requestUpdate();
200227
}
201228

@@ -266,6 +293,10 @@ export class DemoBody extends LitElement {
266293
// Set up accordion state persistence
267294
this._setupAccordionStateManagement();
268295

296+
// Inform parent components of initial state so they can hydrate their UI
297+
this._dispatchSettingsChangeEvent();
298+
this._dispatchChatInstanceChangeEvent();
299+
269300
// Set data attribute for CSS styling
270301
this.setAttribute(
271302
"data-set-chat-config-mode",
@@ -388,6 +419,8 @@ export class DemoBody extends LitElement {
388419

389420
this.settings = newSettings;
390421

422+
this._dispatchSettingsChangeEvent();
423+
391424
// Update query parameters with new settings
392425
this._updateQueryParamsForSettings(newSettings, shouldRefresh);
393426

demo/src/framework/demo-header.ts

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,134 @@
88
*/
99

1010
import { html, LitElement } from "lit";
11-
import { customElement } from "lit/decorators.js";
11+
import { customElement, property } from "lit/decorators.js";
12+
import { iconLoader } from "@carbon/web-components/es/globals/internal/icon-loader.js";
13+
import AiLaunch20 from "@carbon/icons/es/ai-launch/20.js";
14+
import Switcher20 from "@carbon/icons/es/switcher/20.js";
15+
import { ViewType, type ChatInstance } from "@carbon/ai-chat";
16+
17+
import type { Settings } from "./types";
1218

1319
/**
1420
* `DemoHeader` is a custom Lit element representing a header component.
1521
*/
1622
@customElement("demo-header")
1723
export class DemoHeader extends LitElement {
24+
@property({ attribute: false })
25+
accessor settings: Settings | undefined = undefined;
26+
27+
@property({ attribute: false })
28+
accessor chatInstance: ChatInstance | null = null;
29+
30+
private _clickInProgress = false;
31+
32+
onClick = async () => {
33+
if (this.chatInstance) {
34+
const state = this.chatInstance?.getState();
35+
if (state.viewState.mainWindow) {
36+
await this.chatInstance.changeView(ViewType.LAUNCHER);
37+
} else {
38+
await this.chatInstance.changeView(ViewType.MAIN_WINDOW);
39+
}
40+
}
41+
};
42+
43+
private _handleButtonClick = async () => {
44+
if (this._clickInProgress) {
45+
return;
46+
}
47+
48+
this._clickInProgress = true;
49+
this.requestUpdate();
50+
51+
try {
52+
await this.onClick();
53+
} finally {
54+
this._clickInProgress = false;
55+
this.requestUpdate();
56+
}
57+
};
58+
59+
private _handlePanelToggle = (event: Event) => {
60+
const target = event.currentTarget as HTMLElement | null;
61+
if (!target) {
62+
return;
63+
}
64+
65+
const panelId = target.getAttribute("panel-id");
66+
if (!panelId) {
67+
return;
68+
}
69+
70+
const panel = this.renderRoot?.querySelector<HTMLElement>(`#${panelId}`);
71+
if (!panel) {
72+
return;
73+
}
74+
75+
if (panel.hasAttribute("expanded")) {
76+
panel.removeAttribute("expanded");
77+
} else {
78+
panel.setAttribute("expanded", "");
79+
}
80+
};
81+
1882
render() {
83+
const chatInstanceReady = this.chatInstance ? true : false;
84+
const isFullscreen = Boolean(
85+
this.settings?.layout === "fullscreen" ||
86+
this.settings?.layout === "fullscreen-no-gutter",
87+
);
88+
1989
return html`
2090
<cds-header
2191
aria-label="Carbon AI Chat"
22-
style="height: 40px; box-sizing: content-box;"
92+
style="${isFullscreen ? `height: 40px; box-sizing: content-box;` : ""}"
2393
>
2494
<cds-header-name
2595
href="https://chat.carbondesignsystem.com/tag/latest/docs/documents/Overview.html"
2696
prefix="Carbon"
2797
>AI chat</cds-header-name
2898
>
99+
<div class="cds--header__global">
100+
${chatInstanceReady
101+
? html`<cds-header-global-action
102+
aria-label="Open AI Chat"
103+
tooltip-text="Open AI Chat"
104+
?disabled=${this._clickInProgress}
105+
@click=${this._handleButtonClick}
106+
>
107+
${iconLoader(AiLaunch20, { slot: "icon" })}
108+
</cds-header-global-action>`
109+
: ""}
110+
<cds-header-global-action
111+
aria-label="Open Resources Panel"
112+
tooltip-text="Open Resources Panel"
113+
tooltip-alignment="right"
114+
panel-id="switcher-panel"
115+
@click=${this._handlePanelToggle}
116+
>
117+
${iconLoader(Switcher20, { slot: "icon" })}
118+
</cds-header-global-action>
119+
</div>
120+
<cds-header-panel id="switcher-panel" aria-label="Resources Panel">
121+
<cds-switcher aria-label="Resources">
122+
<cds-switcher-item
123+
aria-label="Documentation site"
124+
href="https://chat.carbondesignsystem.com/tag/latest/docs/documents/Overview.html"
125+
>Documentation site</cds-switcher-item
126+
>
127+
<cds-switcher-item
128+
aria-label="React examples"
129+
href="https://github.com/carbon-design-system/carbon-ai-chat/tree/main/examples/react"
130+
>React examples</cds-switcher-item
131+
>
132+
<cds-switcher-item
133+
aria-label="Web component examples"
134+
href="https://github.com/carbon-design-system/carbon-ai-chat/tree/main/examples/web-components"
135+
>Web component examples</cds-switcher-item
136+
>
137+
</cds-switcher>
138+
</cds-header-panel>
29139
</cds-header>
30140
`;
31141
}

demo/src/framework/demo-side-bar-nav.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

demo/src/main.ts

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import "./framework/demo-layout-switcher";
1919
import "./framework/demo-homescreen-switcher";
2020
import "./framework/demo-theme-switcher";
2121
import "./framework/demo-page-theme-switcher";
22-
import "./framework/demo-side-bar-nav";
2322
import "./framework/demo-writeable-elements-switcher";
2423
import "./web-components/demo-app";
2524

26-
import { PublicConfig } from "@carbon/ai-chat";
27-
import { html, LitElement, css } from "lit";
25+
import { ChatInstance, PublicConfig } from "@carbon/ai-chat";
26+
import { html, LitElement, PropertyValues, css } from "lit";
2827
import { customElement, state } from "lit/decorators.js";
2928

3029
import { Settings } from "./framework/types";
@@ -67,13 +66,25 @@ export class Demo extends LitElement {
6766
@state()
6867
accessor hasReceivedSetChatConfig: boolean = false;
6968

69+
private headerSettings?: Settings;
70+
private headerChatInstance: ChatInstance | null = null;
71+
private headerSlot: HTMLSlotElement | null = null;
72+
7073
connectedCallback() {
7174
super.connectedCallback();
7275
// Listen for setChatConfig mode changes from demo-body
7376
this.addEventListener(
7477
"set-chat-config-mode-changed",
7578
this._onSetChatConfigModeChanged as EventListener,
7679
);
80+
this.addEventListener(
81+
"demo-settings-changed",
82+
this._onDemoSettingsChanged as EventListener,
83+
);
84+
this.addEventListener(
85+
"demo-chat-instance-changed",
86+
this._onDemoChatInstanceChanged as EventListener,
87+
);
7788
}
7889

7990
disconnectedCallback() {
@@ -82,6 +93,19 @@ export class Demo extends LitElement {
8293
"set-chat-config-mode-changed",
8394
this._onSetChatConfigModeChanged as EventListener,
8495
);
96+
this.removeEventListener(
97+
"demo-settings-changed",
98+
this._onDemoSettingsChanged as EventListener,
99+
);
100+
this.removeEventListener(
101+
"demo-chat-instance-changed",
102+
this._onDemoChatInstanceChanged as EventListener,
103+
);
104+
this.headerSlot?.removeEventListener(
105+
"slotchange",
106+
this._onHeaderSlotChange,
107+
);
108+
this.headerSlot = null;
85109
}
86110

87111
private _onSetChatConfigModeChanged = (event: Event) => {
@@ -90,6 +114,72 @@ export class Demo extends LitElement {
90114
this.hasReceivedSetChatConfig = customEvent.detail.hasReceivedSetChatConfig;
91115
};
92116

117+
private _onDemoSettingsChanged = (event: Event) => {
118+
const customEvent = event as CustomEvent<{ settings: Settings }>;
119+
this.headerSettings = customEvent.detail.settings;
120+
this._applyHeaderStateToComponent();
121+
};
122+
123+
private _onDemoChatInstanceChanged = (event: Event) => {
124+
const customEvent = event as CustomEvent<{
125+
chatInstance: ChatInstance | null;
126+
}>;
127+
this.headerChatInstance = customEvent.detail.chatInstance ?? null;
128+
this._applyHeaderStateToComponent();
129+
};
130+
131+
private _onHeaderSlotChange = () => {
132+
this._applyHeaderStateToComponent();
133+
};
134+
135+
protected firstUpdated(_changedProperties: PropertyValues): void {
136+
super.firstUpdated(_changedProperties);
137+
this.headerSlot = this.shadowRoot?.querySelector(
138+
'slot[name="demo-header"]',
139+
) as HTMLSlotElement | null;
140+
this.headerSlot?.addEventListener("slotchange", this._onHeaderSlotChange);
141+
this._applyHeaderStateToComponent();
142+
}
143+
144+
private _getDemoHeaderElement(): HTMLElement | null {
145+
const slot =
146+
this.headerSlot ??
147+
(this.shadowRoot?.querySelector(
148+
'slot[name="demo-header"]',
149+
) as HTMLSlotElement | null);
150+
if (!slot) {
151+
return null;
152+
}
153+
154+
const assignedElements = slot.assignedElements({ flatten: true });
155+
for (const element of assignedElements) {
156+
if (!(element instanceof HTMLElement)) {
157+
continue;
158+
}
159+
160+
if (element.tagName.toLowerCase() === "demo-header") {
161+
return element;
162+
}
163+
164+
const nestedHeader = element.querySelector("demo-header");
165+
if (nestedHeader) {
166+
return nestedHeader as HTMLElement;
167+
}
168+
}
169+
170+
return null;
171+
}
172+
173+
private _applyHeaderStateToComponent() {
174+
const headerElement = this._getDemoHeaderElement();
175+
if (!headerElement) {
176+
return;
177+
}
178+
179+
(headerElement as any).settings = this.headerSettings;
180+
(headerElement as any).chatInstance = this.headerChatInstance;
181+
}
182+
93183
private _leaveSetChatConfigMode = () => {
94184
// Remove setChatConfig query parameters and reload
95185
const urlParams = new URLSearchParams(window.location.search);

0 commit comments

Comments
 (0)