Skip to content

Commit b41436c

Browse files
authored
Merge branch 'main' into feature/composer-ref-focus-api-clean
2 parents 1d6a868 + 29fde4f commit b41436c

File tree

94 files changed

+1134
-177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+1134
-177
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
121121
- `@msinternal/botframework-webchat-react-hooks` for helpers for React hooks
122122
- Added link sanitization and ESLint rules, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim)
123123
- Added blob URL sanitization and ESLint rules, in PR [#5568](https://github.com/microsoft/BotFramework-WebChat/pull/5568), by [@compulim](https://github.com/compulim)
124-
- Added visual message grouping following the `isPartOf` property of the `Message` entity, in PR [#5553](https://github.com/microsoft/BotFramework-WebChat/pull/5553), in PR [#5585](https://github.com/microsoft/BotFramework-WebChat/pull/5585), by [@OEvgeny](https://github.com/OEvgeny)
124+
- Added visual message grouping following the `isPartOf` property of the `Message` entity, in PR [#5553](https://github.com/microsoft/BotFramework-WebChat/pull/5553), in PR [#5585](https://github.com/microsoft/BotFramework-WebChat/pull/5585), in PR [#5590](https://github.com/microsoft/BotFramework-WebChat/pull/5590), by [@OEvgeny](https://github.com/OEvgeny)
125125
- The mode is suitable for providing chain-of-thought reasoning
126126
- Added visual indication of `creativeWorkStatus` property in `Message` entity:
127127
- `undefined` - no indicator is shown
@@ -142,6 +142,8 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
142142
- Bundling vendor chunks, by [@compulim](https://github.com/compulim) in PR [#5595](https://github.com/microsoft/BotFramework-WebChat/pull/5595)
143143
- Added deprecation notes for legacy imports, by [@compulim](https://github.com/compulim) in PR [#5600](https://github.com/microsoft/BotFramework-WebChat/pull/5600)
144144
- `import { hooks } from 'botframework-webchat'` should be replaced by `import * as hooks from 'botframework-webchat/hook'`
145+
- Added target to Chrome 100 and re-enable Lightning CSS for ESM builds, by [@compulim](https://github.com/compulim) in PR [#5602](https://github.com/microsoft/BotFramework-WebChat/pull/5602)
146+
- Relaxed `role` prop to allow any string instead of ARIA landmark roles, in PR [#5561](https://github.com/microsoft/BotFramework-WebChat/pull/5561), by [@compulim](https://github.com/compulim)
145147

146148
### Changed
147149

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/* eslint-env browser */
2+
3+
// #region TODO: Remove me after we bump Chrome to v117+
4+
const customElementNames = customElements.getName instanceof Function ? null : new WeakMap();
5+
6+
export function getCustomElementName(customElementConstructor) {
7+
if (customElementNames) {
8+
return customElementNames.get(customElementConstructor);
9+
}
10+
return customElements.getName(customElementConstructor);
11+
}
12+
13+
function setCustomElementName(customElementConstructor, name) {
14+
if (customElementNames) {
15+
customElementNames.set(customElementConstructor, name);
16+
}
17+
// No need to set for browsers that support customElements.getName()
18+
}
19+
// #endregion
20+
21+
export function customElement(elementKey, createElementClass) {
22+
const elementRegistration = document.querySelector(`element-registration[element-key="${elementKey}"]`);
23+
elementRegistration.elementConstructor = createElementClass(elementRegistration);
24+
}
25+
26+
function addSourceMapToExtractedScript(scriptContent, originalFileUrl) {
27+
const sourceMap = {
28+
version: 3,
29+
sources: [originalFileUrl],
30+
names: [],
31+
mappings: 'AAAA', // Simple mapping - entire script maps to original file
32+
file: originalFileUrl.split('/').pop(),
33+
sourceRoot: '',
34+
sourcesContent: [scriptContent]
35+
};
36+
37+
const base64Map = btoa(JSON.stringify(sourceMap));
38+
const dataUrl = `data:application/json;charset=utf-8;base64,${base64Map}`;
39+
40+
// TODO: Figure out how to make setting breakpoints work
41+
return scriptContent + `\n//# sourceMappingURL=${dataUrl}`;
42+
}
43+
44+
function fixScript(script, url) {
45+
const newScript = document.createElement('script');
46+
47+
Array.from(script.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
48+
newScript.text = addSourceMapToExtractedScript(script.text, url);
49+
50+
return newScript;
51+
}
52+
53+
function initDocument(elementRegistration, currentDocument) {
54+
const moduleUrl = new URL(`./${elementRegistration.getAttribute('element-key')}.ce.js`, import.meta.url).toString();
55+
const allowedElementNames = ['link', 'style', 'script'];
56+
57+
if (!currentDocument) {
58+
throw new Error('Custom element must be registered within a <element-registration> element.');
59+
}
60+
61+
const result = Promise.withResolvers();
62+
63+
Object.defineProperty(elementRegistration, 'elementConstructor', {
64+
set(constructor) {
65+
if (!constructor) {
66+
throw new Error('Custom element constructor is required.');
67+
}
68+
69+
const elementName = elementRegistration.getAttribute('element-name');
70+
71+
if (!elementName) {
72+
throw new Error('Custom element must have a name.');
73+
}
74+
75+
customElements.define(elementName, constructor, constructor.options);
76+
setCustomElementName(constructor, elementName);
77+
78+
result.resolve(constructor);
79+
},
80+
get() {
81+
return customElement.get(elementRegistration.getAttribute('element-name'));
82+
}
83+
});
84+
85+
document.head.append(
86+
...Array.from(currentDocument.head.children)
87+
.filter(element => allowedElementNames.includes(element.localName))
88+
.map(element => (element.localName === 'script' ? fixScript(element, moduleUrl) : element))
89+
);
90+
91+
elementRegistration.append(
92+
...Array.from(currentDocument.body.children).map(element =>
93+
element.localName === 'script' ? fixScript(element, moduleUrl) : element
94+
)
95+
);
96+
document.body.appendChild(elementRegistration);
97+
98+
return result.promise;
99+
}
100+
101+
export function registerElements(...elementNames) {
102+
const parser = new DOMParser();
103+
const entries = elementNames.map(entry => (typeof entry === 'string' ? [entry, entry] : Object.entries(entry).at(0)));
104+
105+
const raceInit = (key, initPromise) =>
106+
Promise.race([
107+
new Promise((_resolve, reject) => {
108+
setTimeout(
109+
() => reject(new Error(`Could not initialize custom element "${key}". Did you call customElement()?`)),
110+
5000
111+
);
112+
}),
113+
initPromise
114+
]);
115+
116+
return Promise.all(
117+
entries.map(async ([key, elementName]) => {
118+
const content = await fetch(new URL(`./${key}.ce`, import.meta.url)).then(response => response.text());
119+
120+
const elementRegistration = document.createElement('element-registration');
121+
elementRegistration.setAttribute('element-key', key);
122+
elementRegistration.setAttribute('element-name', elementName);
123+
124+
return raceInit(key, initDocument(elementRegistration, parser.parseFromString(content, 'text/html')));
125+
})
126+
);
127+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Event Stream Custom Element</title>
7+
</head>
8+
<body>
9+
<template>
10+
<style>
11+
.event-list {
12+
align-items: center;
13+
display: flex;
14+
gap: 0.5em;
15+
left: 0.5em;
16+
padding: 0.5em;
17+
position: fixed;
18+
top: 0.5em;
19+
z-index: 1;
20+
font-size: 3rem;
21+
counter-reset: event-count;
22+
}
23+
.event-list > * {
24+
counter-increment: event-count;
25+
--event-count: counter(event-count);
26+
}
27+
.event-list > *:nth-last-child(n + 4) {
28+
pointer-events: none;
29+
position: absolute;
30+
visibility: hidden;
31+
inset: 0 100% 100% 0;
32+
}
33+
</style>
34+
<div class="event-list"></div>
35+
</template>
36+
<script type="module">
37+
import { customElement } from '/assets/custom-element/custom-element.js';
38+
39+
customElement('event-stream', currentDocument =>
40+
class EventStreamElement extends HTMLElement {
41+
constructor() {
42+
super();
43+
const template = currentDocument.querySelector('template');
44+
const fragment = template.content.cloneNode(true);
45+
46+
const shadowRoot = this.attachShadow({ mode: 'open' });
47+
this.eventList = fragment.querySelector('.event-list');
48+
shadowRoot.appendChild(fragment);
49+
}
50+
51+
connectedCallback() {
52+
this.controller = new AbortController();
53+
document.addEventListener('event-stream:event', this.handleEvent, { signal: this.controller.signal });
54+
}
55+
56+
disconnectedCallback() {
57+
this.controller.abort();
58+
}
59+
60+
handleEvent = event => {
61+
const { content } = event.detail;
62+
63+
// TODO: Remove when we reach Chrome v133+
64+
content.style.setProperty('--event-count', this.eventList.children.length);
65+
66+
this.eventList.append(content);
67+
};
68+
}
69+
);
70+
</script>
71+
</body>
72+
</html>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Keyboard Event Custom Element</title>
6+
</head>
7+
<body>
8+
<template>
9+
<style>
10+
/* TODO this is not needed after Chrome v133+, just use var(--event-count) */
11+
:host kbd::after {
12+
counter-set: event-index var(--event-count);
13+
--event-counter: counter(event-index);
14+
}
15+
16+
kbd {
17+
background-color: #f7f7f7;
18+
border-radius: 6px;
19+
border: 1px solid #ccc;
20+
box-shadow: 0 1px 0 #aaaaaa44, 0 2px 0 #ffffff44 inset;
21+
color: #3e3e3e;
22+
font-family: ui-monospace;
23+
font-weight: bold;
24+
line-height: 1;
25+
padding: 0.2em 0.4em;
26+
position: relative;
27+
text-align: center;
28+
}
29+
30+
kbd::after {
31+
content: var(--event-counter, var(--event-count));
32+
font-size: 16px;
33+
inset: 6px auto auto 6px;
34+
position: absolute;
35+
}
36+
37+
:host(:last-of-type) kbd {
38+
background-color: #e4e8ec;
39+
}
40+
41+
kbd[data-key]::before {
42+
content: attr(data-key);
43+
display: inline-block;
44+
height: 1em;
45+
text-align: center;
46+
width: 1em;
47+
}
48+
kbd[data-key="Enter"]::before {
49+
content: "⏎";
50+
}
51+
kbd[data-key="Backspace"]::before {
52+
content: "⌫";
53+
}
54+
kbd[data-key="Tab"]::before {
55+
content: "⇥";
56+
}
57+
kbd[data-key="Escape"]::before {
58+
content: "⎋";
59+
}
60+
kbd[data-key=" "]::before {
61+
content: "␣";
62+
}
63+
kbd[data-key="ArrowUp"]::before {
64+
content: "↑";
65+
}
66+
kbd[data-key="ArrowDown"]::before {
67+
content: "↓";
68+
}
69+
kbd[data-key="ArrowLeft"]::before {
70+
content: "←";
71+
}
72+
kbd[data-key="ArrowRight"]::before {
73+
content: "→";
74+
}
75+
kbd[data-key="Delete"]::before {
76+
content: "⌦";
77+
}
78+
kbd[data-key="Home"]::before {
79+
content: "⇱";
80+
}
81+
kbd[data-key="End"]::before {
82+
content: "⇲";
83+
}
84+
kbd[data-key="PageUp"]::before {
85+
content: "⇈";
86+
}
87+
kbd[data-key="PageDown"]::before {
88+
content: "⇊";
89+
}
90+
kbd[data-key="Insert"]::before {
91+
content: "⎀";
92+
}
93+
kbd[data-key="Meta"]::before {
94+
content: "⌘";
95+
}
96+
kbd[data-key="Control"]::before {
97+
content: "⌃";
98+
}
99+
kbd[data-key="Alt"]::before {
100+
content: "⎇";
101+
}
102+
kbd[data-key="Shift"]::before {
103+
content: "⇧";
104+
}
105+
</style>
106+
<kbd></kbd>
107+
</template>
108+
<script type="module">
109+
import { customElement, getCustomElementName } from '/assets/custom-element/custom-element.js';
110+
111+
customElement('keyboard-event', currentDocument =>
112+
class KeyboardEventElement extends HTMLElement {
113+
constructor() {
114+
super();
115+
const template = currentDocument.querySelector('template');
116+
const fragment = template.content.cloneNode(true);
117+
118+
const shadowRoot = this.attachShadow({ mode: 'open' });
119+
shadowRoot.appendChild(fragment);
120+
121+
this.kbd = shadowRoot.querySelector('kbd');
122+
}
123+
124+
set key(value) {
125+
this.kbd.setAttribute('data-key', value);
126+
}
127+
128+
get key() {
129+
return this.kbd.getAttribute('data-key');
130+
}
131+
132+
static listenKeyboardEvents(scope = window) {
133+
// const myTagName = customElements.getName(this)
134+
const myTagName = getCustomElementName(this);
135+
const abortController = new AbortController();
136+
scope.addEventListener('keydown', event => {
137+
const element = document.createElement(myTagName);
138+
element.key = event.key;
139+
document.dispatchEvent(new CustomEvent('event-stream:event', {
140+
detail: {
141+
content: element
142+
}
143+
}));
144+
}, { capture: true, signal: abortController.signal });
145+
146+
return abortController;
147+
}
148+
}
149+
)
150+
</script>
151+
</body>
152+
</html>

0 commit comments

Comments
 (0)