Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7d23477
Revert "Fix typings for FullChatComposer (#5580)"
OEvgeny Sep 22, 2025
428c815
test: add tests for keyboard collaps/expand
OEvgeny Sep 22, 2025
a8f52be
feat: support a11y for activity button
OEvgeny Sep 22, 2025
53fd424
feat: cleanup and improve custom properties
OEvgeny Sep 22, 2025
aa83ffd
Add message status outside of group test
OEvgeny Sep 24, 2025
7bc8e33
Add support for <script type="module"> (a.k.a. fat module) (#5592)
compulim Sep 24, 2025
a96270c
Support `<script type="module">` imports for Fluent skin pack (#5593)
compulim Sep 26, 2025
86ba85f
Print commit stats (#5596)
compulim Sep 28, 2025
cb43a9c
Reorganize `/static` through vendor chunks (#5595)
compulim Sep 28, 2025
d385e29
Add delay to --watch and better build validation (#5599)
compulim Sep 29, 2025
36f4c1d
Show deprecation for legacy imports (#5600)
compulim Sep 29, 2025
c566238
Allow any ARIA role attribute/prop (#5601)
compulim Sep 29, 2025
55c7a6b
Fix message status tests
OEvgeny Sep 29, 2025
c1724cc
Merge branch 'main', remote-tracking branch 'origin' into feat/groupi…
OEvgeny Sep 30, 2025
2f3af68
Merge remote-tracking branch 'origin' into feat/grouping-refine-2
OEvgeny Sep 30, 2025
cd6693c
Fix more
OEvgeny Sep 30, 2025
d253c6f
More test fixes
OEvgeny Sep 30, 2025
061e914
Revert unwanted
OEvgeny Sep 30, 2025
eb28f73
Changelog
OEvgeny Sep 30, 2025
4e16dc2
Fix collapsible group content jump
OEvgeny Oct 1, 2025
42c6505
Fix new custom elements
OEvgeny Oct 1, 2025
96cfd27
Update tests
OEvgeny Oct 1, 2025
092175b
Another fix for content cut
OEvgeny Oct 1, 2025
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- `@msinternal/botframework-webchat-react-hooks` for helpers for React hooks
- Added link sanitization and ESLint rules, in PR [#5564](https://github.com/microsoft/BotFramework-WebChat/pull/5564), by [@compulim](https://github.com/compulim)
- Added blob URL sanitization and ESLint rules, in PR [#5568](https://github.com/microsoft/BotFramework-WebChat/pull/5568), by [@compulim](https://github.com/compulim)
- 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)
- 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)
- The mode is suitable for providing chain-of-thought reasoning
- Added visual indication of `creativeWorkStatus` property in `Message` entity:
- `undefined` - no indicator is shown
Expand Down
127 changes: 127 additions & 0 deletions __tests__/html/assets/custom-element/custom-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* eslint-env browser */

// #region TODO: Remove me after we bump Chrome to v117+
const customElementNames = customElements.getName instanceof Function ? null : new WeakMap();

export function getCustomElementName(customElementConstructor) {
if (customElementNames) {
return customElementNames.get(customElementConstructor);
}
return customElements.getName(customElementConstructor);
}

function setCustomElementName(customElementConstructor, name) {
if (customElementNames) {
customElementNames.set(customElementConstructor, name);
}
// No need to set for browsers that support customElements.getName()
}
// #endregion

export function customElement(elementKey, createElementClass) {
const elementRegistration = document.querySelector(`element-registration[element-key="${elementKey}"]`);
elementRegistration.elementConstructor = createElementClass(elementRegistration);
}

function addSourceMapToExtractedScript(scriptContent, originalFileUrl) {
const sourceMap = {
version: 3,
sources: [originalFileUrl],
names: [],
mappings: 'AAAA', // Simple mapping - entire script maps to original file
file: originalFileUrl.split('/').pop(),
sourceRoot: '',
sourcesContent: [scriptContent]
};

const base64Map = btoa(JSON.stringify(sourceMap));
const dataUrl = `data:application/json;charset=utf-8;base64,${base64Map}`;

// TODO: Figure out how to make setting breakpoints work
return scriptContent + `\n//# sourceMappingURL=${dataUrl}`;
}

function fixScript(script, url) {
const newScript = document.createElement('script');

Array.from(script.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
newScript.text = addSourceMapToExtractedScript(script.text, url);

return newScript;
}

function initDocument(elementRegistration, currentDocument) {
const moduleUrl = new URL(`./${elementRegistration.getAttribute('element-key')}.ce.js`, import.meta.url).toString();
const allowedElementNames = ['link', 'style', 'script'];

if (!currentDocument) {
throw new Error('Custom element must be registered within a <element-registration> element.');
}

const result = Promise.withResolvers();

Object.defineProperty(elementRegistration, 'elementConstructor', {
set(constructor) {
if (!constructor) {
throw new Error('Custom element constructor is required.');
}

const elementName = elementRegistration.getAttribute('element-name');

if (!elementName) {
throw new Error('Custom element must have a name.');
}

customElements.define(elementName, constructor, constructor.options);
setCustomElementName(constructor, elementName);

result.resolve(constructor);
},
get() {
return customElement.get(elementRegistration.getAttribute('element-name'));
}
});

document.head.append(
...Array.from(currentDocument.head.children)
.filter(element => allowedElementNames.includes(element.localName))
.map(element => (element.localName === 'script' ? fixScript(element, moduleUrl) : element))
);

elementRegistration.append(
...Array.from(currentDocument.body.children).map(element =>
element.localName === 'script' ? fixScript(element, moduleUrl) : element
)
);
document.body.appendChild(elementRegistration);

return result.promise;
}

export function registerElements(...elementNames) {
const parser = new DOMParser();
const entries = elementNames.map(entry => (typeof entry === 'string' ? [entry, entry] : Object.entries(entry).at(0)));

const raceInit = (key, initPromise) =>
Promise.race([
new Promise((_resolve, reject) => {
setTimeout(
() => reject(new Error(`Could not initialize custom element "${key}". Did you call customElement()?`)),
5000
);
}),
initPromise
]);

return Promise.all(
entries.map(async ([key, elementName]) => {
const content = await fetch(new URL(`./${key}.ce`, import.meta.url)).then(response => response.text());

const elementRegistration = document.createElement('element-registration');
elementRegistration.setAttribute('element-key', key);
elementRegistration.setAttribute('element-name', elementName);

return raceInit(key, initDocument(elementRegistration, parser.parseFromString(content, 'text/html')));
})
);
}
72 changes: 72 additions & 0 deletions __tests__/html/assets/custom-element/event-stream.ce.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Stream Custom Element</title>
</head>
<body>
<template>
<style>
.event-list {
align-items: center;
display: flex;
gap: 0.5em;
left: 0.5em;
padding: 0.5em;
position: fixed;
top: 0.5em;
z-index: 1;
font-size: 3rem;
counter-reset: event-count;
}
.event-list > * {
counter-increment: event-count;
--event-count: counter(event-count);
}
.event-list > *:nth-last-child(n + 4) {
pointer-events: none;
position: absolute;
visibility: hidden;
inset: 0 100% 100% 0;
}
</style>
<div class="event-list"></div>
</template>
<script type="module">
import { customElement } from '/assets/custom-element/custom-element.js';

customElement('event-stream', currentDocument =>
class EventStreamElement extends HTMLElement {
constructor() {
super();
const template = currentDocument.querySelector('template');
const fragment = template.content.cloneNode(true);

const shadowRoot = this.attachShadow({ mode: 'open' });
this.eventList = fragment.querySelector('.event-list');
shadowRoot.appendChild(fragment);
}

connectedCallback() {
this.controller = new AbortController();
document.addEventListener('event-stream:event', this.handleEvent, { signal: this.controller.signal });
}

disconnectedCallback() {
this.controller.abort();
}

handleEvent = event => {
const { content } = event.detail;

// TODO: Remove when we reach Chrome v133+
content.style.setProperty('--event-count', this.eventList.children.length);

this.eventList.append(content);
};
}
);
</script>
</body>
</html>
152 changes: 152 additions & 0 deletions __tests__/html/assets/custom-element/keyboard-event.ce.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keyboard Event Custom Element</title>
</head>
<body>
<template>
<style>
/* TODO this is not needed after Chrome v133+, just use var(--event-count) */
:host kbd::after {
counter-set: event-index var(--event-count);
--event-counter: counter(event-index);
}

kbd {
background-color: #f7f7f7;
border-radius: 6px;
border: 1px solid #ccc;
box-shadow: 0 1px 0 #aaaaaa44, 0 2px 0 #ffffff44 inset;
color: #3e3e3e;
font-family: ui-monospace;
font-weight: bold;
line-height: 1;
padding: 0.2em 0.4em;
position: relative;
text-align: center;
}

kbd::after {
content: var(--event-counter, var(--event-count));
font-size: 16px;
inset: 6px auto auto 6px;
position: absolute;
}

:host(:last-of-type) kbd {
background-color: #e4e8ec;
}

kbd[data-key]::before {
content: attr(data-key);
display: inline-block;
height: 1em;
text-align: center;
width: 1em;
}
kbd[data-key="Enter"]::before {
content: "⏎";
}
kbd[data-key="Backspace"]::before {
content: "⌫";
}
kbd[data-key="Tab"]::before {
content: "⇥";
}
kbd[data-key="Escape"]::before {
content: "⎋";
}
kbd[data-key=" "]::before {
content: "␣";
}
kbd[data-key="ArrowUp"]::before {
content: "↑";
}
kbd[data-key="ArrowDown"]::before {
content: "↓";
}
kbd[data-key="ArrowLeft"]::before {
content: "←";
}
kbd[data-key="ArrowRight"]::before {
content: "→";
}
kbd[data-key="Delete"]::before {
content: "⌦";
}
kbd[data-key="Home"]::before {
content: "⇱";
}
kbd[data-key="End"]::before {
content: "⇲";
}
kbd[data-key="PageUp"]::before {
content: "⇈";
}
kbd[data-key="PageDown"]::before {
content: "⇊";
}
kbd[data-key="Insert"]::before {
content: "⎀";
}
kbd[data-key="Meta"]::before {
content: "⌘";
}
kbd[data-key="Control"]::before {
content: "⌃";
}
kbd[data-key="Alt"]::before {
content: "⎇";
}
kbd[data-key="Shift"]::before {
content: "⇧";
}
</style>
<kbd></kbd>
</template>
<script type="module">
import { customElement, getCustomElementName } from '/assets/custom-element/custom-element.js';

customElement('keyboard-event', currentDocument =>
class KeyboardEventElement extends HTMLElement {
constructor() {
super();
const template = currentDocument.querySelector('template');
const fragment = template.content.cloneNode(true);

const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(fragment);

this.kbd = shadowRoot.querySelector('kbd');
}

set key(value) {
this.kbd.setAttribute('data-key', value);
}

get key() {
return this.kbd.getAttribute('data-key');
}

static listenKeyboardEvents(scope = window) {
// const myTagName = customElements.getName(this)
const myTagName = getCustomElementName(this);
const abortController = new AbortController();
scope.addEventListener('keydown', event => {
const element = document.createElement(myTagName);
element.key = event.key;
document.dispatchEvent(new CustomEvent('event-stream:event', {
detail: {
content: element
}
}));
}, { capture: true, signal: abortController.signal });

return abortController;
}
}
)
</script>
</body>
</html>
10 changes: 10 additions & 0 deletions __tests__/html2/activity/message-status.copilot.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Message status (copilot)</title>
<script>
location = './message-status?variant=copilot';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions __tests__/html2/activity/message-status.fluent.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Message status (fluent)</title>
<script>
location = './message-status?variant=fluent';
</script>
</head>
<body></body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading