Skip to content
Merged
80 changes: 44 additions & 36 deletions packages/browser/src/report-dialog-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
// /* or inside the shadow root: showReportDialog({ css: '.bugsee-reporting-dialog-send { … }' }) */
//
// Colours are the mobile SDKs' (Android `ColorConstants`, iOS `BugseeTheme`): the light page is #EDF1F6
// with white fields, the dark page #121212, the accent red #FF5858, the severity icons green/amber/
// salmon/red/dark-red. Two deliberate adjustments for the web, both for contrast: the light text is a
// with white fields, the dark page #121212, the accent was red #FF5858 (now the viewer's brand teal), the severity icons green/amber/
// salmon/red/dark-red. The accent is the viewer's brand teal (#60B4C4; #3A8A99 on the light page, the
// viewer's AA-contrast variant). Two deliberate adjustments for the web, both for contrast: the light text is a
// shade darker than Android's #637E99 (4.2:1 on white) and the dark field is #262626 rather than #777777.

/** The prefix of every class, part and custom property the dialog exposes. */
Expand All @@ -35,13 +36,13 @@ export const REPORT_DIALOG_CSS = `
--${p}field-border: rgba(153, 153, 153, 0.4);
--${p}text: #4F6A85;
--${p}hint: #8497AB;
--${p}accent: #FF5858;
--${p}accent: #3A8A99;
--${p}error: #C62828;
--${p}severity-active: #182433;
--${p}severity-idle: #637E99;
--${p}shade: rgba(0, 0, 0, 0.55);
--${p}shade: rgba(0, 0, 0, 0.92);
--${p}shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
--${p}radius: 8px;
--${p}radius: 12px;
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
font: 17px/1.35 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
:host([data-theme='dark']) {
Expand All @@ -51,6 +52,7 @@ export const REPORT_DIALOG_CSS = `
--${p}field-border: #555555;
--${p}text: #FFFFFF;
--${p}hint: #B4B4B4;
--${p}accent: #60B4C4;
--${p}error: #FF8A80;
--${p}severity-active: #FFFFFF;
--${p}severity-idle: #B4B4B4;
Expand All @@ -63,6 +65,7 @@ export const REPORT_DIALOG_CSS = `
--${p}field-border: #555555;
--${p}text: #FFFFFF;
--${p}hint: #B4B4B4;
--${p}accent: #60B4C4;
--${p}error: #FF8A80;
--${p}severity-active: #FFFFFF;
--${p}severity-idle: #B4B4B4;
Expand Down Expand Up @@ -103,6 +106,7 @@ export const REPORT_DIALOG_CSS = `
max-height: 100%;
margin: auto;
background: ${v('page')};
border: 1px solid ${v('field-border')};
border-radius: ${v('radius')};
box-shadow: ${v('shadow')};
overflow: hidden;
Expand All @@ -113,17 +117,17 @@ export const REPORT_DIALOG_CSS = `
align-items: center;
gap: 8px;
height: 56px;
padding: 0 8px 0 4px;
padding: 0 8px 0 8px;
flex: none;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.${p}title {
flex: 1;
margin: 0;
text-align: center;
font-size: 17px;
font-weight: 600;
color: ${v('accent')};
font-weight: 700;
text-transform: uppercase;
color: ${v('text')};
}
.${p}close {
width: 44px;
Expand All @@ -132,25 +136,31 @@ export const REPORT_DIALOG_CSS = `
border: 0;
border-radius: 50%;
background: none;
color: ${v('accent')};
color: ${v('text')};
cursor: pointer;
}
.${p}close-icon { display: block; width: 100%; height: 100%; fill: currentColor; }
.${p}send {
min-width: 64px;
height: 40px;
padding: 0 16px;
width: 44px;
height: 44px;
padding: 9px; /* the arrowhead is a solid shape, so it reads smaller than the thin close glyph at the same size */
border: 0;
border-radius: 3px;
border-radius: 50%;
background: none;
color: ${v('accent')};
font: inherit;
font-size: 16px;
font-weight: 600;
cursor: pointer;
}
.${p}send-icon {
display: block;
width: 100%;
height: 100%;
fill: currentColor;
stroke: currentColor;
stroke-width: 1.6;
stroke-linejoin: round;
}
.${p}send:disabled { opacity: 0.5; cursor: default; }
.${p}close:hover, .${p}send:hover:not(:disabled) { background: rgba(255, 88, 88, 0.1); }
.${p}close:hover, .${p}send:hover:not(:disabled) { background: rgba(128, 128, 128, 0.15); }

.${p}body {
display: flex;
Expand Down Expand Up @@ -180,10 +190,10 @@ export const REPORT_DIALOG_CSS = `
color: ${v('text')};
background: ${v('field')};
border: 1px solid ${v('field-border')};
border-radius: 2px;
border-radius: 8px;
}
.${p}input::placeholder { color: ${v('hint')}; opacity: 1; }
textarea.${p}input { min-height: 7.5em; resize: vertical; }
textarea.${p}input { min-height: 8.2em; resize: vertical; }
.${p}input:focus-visible { outline: 2px solid ${v('accent')}; outline-offset: 1px; }
.${p}input[aria-invalid='true'] { border-color: ${v('error')}; }
.${p}error { min-height: 0; margin: 0; font-size: 13px; color: ${v('error')}; }
Expand Down Expand Up @@ -221,25 +231,23 @@ textarea.${p}input { min-height: 7.5em; resize: vertical; }
.${p}severity-blocker .${p}severity-icon { color: #8C0101; }

.${p}footer-row {
display: flex;
display: grid;
grid-template-columns: auto auto;
justify-content: center;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 High — display: contents on the powered-by <a> makes it unfocusable (keyboard regression).

.bugsee-reporting-dialog-powered is the https://bugsee.com link. display: contents removes its box so the logo/text can be footer-grid items. In current engines that also removes focusability: .focus() is a no-op and Tab skips the control. tabIndex remains 0, so report-dialog.test.ts ("is reached by keyboard like any link") still passes.

Impact: WCAG 2.1.1 (A). Keyboard users cannot open the attribution link. The :focus-visible outlines on .powered-text / .logo never fire. Regression from display: inline-flex.

Scenario: Tab through the dialog in Chrome — close, fields, send, then the link is skipped. Measured on Chrome 154: a normal <a> becomes document.activeElement; <a style="display:contents"> does not. Same bug class: WebKit 255149, Chromium 1366037, Firefox 1553549.

Fix: Give the <a> a real box. Simplest: display: grid on the link itself (logo grid-row: 1 / span 2, text + version in column 2), which also lets you drop display: contents. If version must stay a sibling, use subgrid — not contents on an interactive element. Prove it with a Playwright Tab test in test/report-dialog.e2e.ts; jsdom cannot.

align-items: center;
justify-content: space-between;
gap: 12px;
padding: 0 12px 12px;
column-gap: 10px;
padding: 0 12px 14px;
flex: none;
}
.${p}version { margin: 0; font-size: 12px; color: ${v('hint')}; }
.${p}powered {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: ${v('hint')};
text-decoration: none;
}
.${p}powered:hover { color: ${v('text')}; text-decoration: underline; }
.${p}powered:focus-visible { outline: 2px solid ${v('accent')}; outline-offset: 2px; border-radius: 2px; }
.${p}logo { display: block; height: 14px; width: auto; }
/* The link has no box of its own: its logo and text are laid out on the footer grid, with the version
line under the text. */
.${p}powered { display: contents; font-size: 16px; color: ${v('hint')}; text-decoration: none; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — display: contents on this <a> is not keyboard-focusable in any shipping browser.

.powered is the bugsee.com link (report-dialog.ts builds <a href> + logo + text). display: contents generates no box, so the element is not “being rendered” for HTML focusability. Can I Use reports 0% support for “elements with display: contents are focusable” (current Chromium, Firefox, and Safari).

The :focus-visible outlines on the next two lines therefore never apply. Keyboard users Tab through the form and never land on this control. The existing test (is reached by keyboard like any link) only checks tabIndex === 0 under jsdom, which does not apply this rule.

Give the <a> a real box instead: display: grid (or flex) on the link, and put the version node inside it (optionally aria-hidden so v… does not join the accessible name). Flattening an interactive element into the parent grid is what creates the bug.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 display: contents on the powered-by <a> takes it out of the tab order in shipping Chrome, Safari, and Firefox (crbug 1366037/1428419 still experimental; WebKit 255149; Gecko 1553549). Keyboard users cannot reach https://bugsee.com from the dialog; the :focus-visible outlines below never match. tabIndex === 0 in jsdom is not evidence of focusability.

Do not use display: contents on an interactive element. Grid the <a> itself and move .version inside it (logo spanning two rows, text + version stacked in column 2). Then add a Chromium e2e that Tabs onto a[data-powered].

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1, still open. display: contents on this <a> is unchanged. Keyboard users still cannot Tab to the Bugsee link in shipping Chrome/Safari/Firefox. Grid the <a> itself and move .version inside it; prove it with a Playwright Tab onto a[data-powered].

.${p}logo { grid-column: 1; grid-row: 1 / span 2; display: block; height: 30px; width: auto; }
.${p}powered-text { grid-column: 2; grid-row: 1; font-size: 16px; color: ${v('hint')}; }
.${p}version { grid-column: 2; grid-row: 2; margin: 0; font-size: 9px; color: ${v('hint')}; opacity: 0.3; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 9px + opacity: 0.3 on --hint over --page composites to ~1.28:1 on the light theme (dark ~2:1). The footer version is a specified feature (docs/design/report-dialog.md, previous 12px hint line) and is now unreadable. Drop the extra opacity and keep ≥12px, or use a muted token that still clears ~3:1.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2, still open. 9px + opacity: 0.3 on --hint is still ~1.28:1 on the light page. Drop the extra opacity and keep ≥12px, or a muted token that clears ~3:1.

.${p}powered:hover .${p}powered-text { color: ${v('text')}; text-decoration: underline; }
.${p}powered:focus-visible .${p}powered-text,
.${p}powered:focus-visible .${p}logo { outline: 2px solid ${v('accent')}; outline-offset: 2px; border-radius: 2px; }

.${p}confirm {
position: absolute;
Expand Down
60 changes: 58 additions & 2 deletions packages/browser/src/report-dialog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type ReportDialogUiOptions,
} from './report-dialog';
import { DEFAULT_REPORT_DIALOG_STRINGS } from './report-dialog-strings';
import { REPORT_DIALOG_CSS } from './report-dialog-styles';

const UI: ReportDialogUiOptions = {
summaryRequired: false,
Expand Down Expand Up @@ -262,6 +263,31 @@ describe('layout', () => {
}
});

it('shows the send action as a paper-plane icon with an accessible name, not text', () => {
const { show } = harness();
void show();
const send = q('[data-action="send"]');
expect(send.textContent).toBe('');
expect(send.getAttribute('aria-label')).toBe('Send');
expect(send.getAttribute('title')).toBe('Send');
const icon = send.querySelector('svg.bugsee-reporting-dialog-send-icon');
expect(icon?.getAttribute('aria-hidden')).toBe('true');
expect(icon?.querySelector('path')?.getAttribute('d')?.length).toBeGreaterThan(20);
});

it('rounds the send arrowhead by stroking it with round joins', () => {
expect(REPORT_DIALOG_CSS).toMatch(
/send-icon \{[^}]*stroke: currentColor;[^}]*stroke-linejoin: round;/,
);
});

it('uses the brand teal as the accent, with the AA variant on the light page', () => {
const css = REPORT_DIALOG_CSS;
expect(css).toContain('--bugsee-reporting-dialog-accent: #3A8A99');
expect(css.match(/--bugsee-reporting-dialog-accent: #60B4C4/g)).toHaveLength(2); // dark + auto
expect(css.toLowerCase()).not.toContain('#ff5858');
});

it('shows the SDK version in the footer', () => {
const { show } = harness();
void show();
Expand All @@ -273,7 +299,7 @@ describe('layout', () => {
const { show } = harness();
void show({ strings: { send: 'Absenden', summaryPlaceholder: 'Kurzfassung' } });

expect(q('[data-action="send"]').textContent).toBe('Absenden');
expect(q('[data-action="send"]').getAttribute('aria-label')).toBe('Absenden');
expect(field('summary').placeholder).toBe('Kurzfassung');
expect(field('description').placeholder).toBe(
DEFAULT_REPORT_DIALOG_STRINGS.descriptionPlaceholder,
Expand Down Expand Up @@ -479,7 +505,7 @@ describe('sending', () => {
expect(upload).toHaveBeenCalledTimes(1);
expect(q<HTMLButtonElement>('[data-action="send"]').disabled).toBe(true);
expect(field('summary').disabled).toBe(true);
expect(q('[data-action="send"]').textContent).toBe('Sending…');
expect(q('[data-action="send"]').getAttribute('aria-label')).toBe('Sending…');

finish({ ok: false });
expect(await pending).toEqual({ ok: false });
Expand Down Expand Up @@ -1167,3 +1193,33 @@ describe('lifecycle events', () => {
await expect(pending).resolves.toEqual({ ok: true });
});
});

describe('toolbar buttons', () => {
it('give the close and send actions the same button size', () => {
const css = REPORT_DIALOG_CSS;
const box = (name: string): string => {
const rule = css.match(new RegExp(`\\.bugsee-reporting-dialog-${name} \\{([^}]*)\\}`));
return `${rule?.[1]?.match(/width: (\d+px)/)?.[1]}x${rule?.[1]?.match(/height: (\d+px)/)?.[1]}`;
};
expect(box('send')).toBe('44pxx44px');
expect(box('close')).toBe(box('send'));
});
});

describe('version line', () => {
it('is small and faint next to the "Powered by" text', () => {
const rule =
REPORT_DIALOG_CSS.match(/\.bugsee-reporting-dialog-version \{([^}]*)\}/)?.[1] ?? '';
expect(rule).toContain('font-size: 9px');
expect(rule).toContain('opacity: 0.3');
});
});

describe('page shading', () => {
it('is nearly black, so the page behind the dialog is barely visible', () => {
const alpha = REPORT_DIALOG_CSS.match(
/--bugsee-reporting-dialog-shade: rgba\(0, 0, 0, ([\d.]+)\)/,
)?.[1];
expect(Number(alpha)).toBeGreaterThanOrEqual(0.9);
});
});
18 changes: 15 additions & 3 deletions packages/browser/src/report-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
// Material "bug_report" and "close" glyphs (Apache-2.0).
const BUG_PATH =
'M20 8h-2.81a5.985 5.985 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6 8h-4v-2h4v2zm0-4h-4v-2h4v2z';
// A send arrowhead with a slit in its back edge. It is filled AND stroked (see `.send-icon` in the styles) so
// the corners come out rounded.
const SEND_PATH = 'M4.3 3.9 21.4 12.4 4.3 20.9 5.8 14 15 12.6 5.8 10.8Z';
const CLOSE_PATH =
'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';

Expand Down Expand Up @@ -251,7 +254,8 @@ function showDialog(
const setBusy = (value: boolean): void => {
busy = value;
refs.send.disabled = value;
refs.send.textContent = value ? strings.sending : strings.send;
refs.send.setAttribute('aria-label', value ? strings.sending : strings.send);
refs.send.title = value ? strings.sending : strings.send;
for (const control of Object.values(refs.fields)) {
control.disabled = value;
}
Expand Down Expand Up @@ -509,9 +513,15 @@ function build(
hook(close, 'close');
close.setAttribute('data-action', 'cancel');
close.append(icon(CLOSE_PATH, 'close-icon'));
const send = make('button', { type: 'submit' }, strings.send);
const send = make('button', {
type: 'submit',
'aria-label': strings.send,
title: strings.send,
});
hook(send, 'send');
send.setAttribute('data-action', 'send');
const sendIcon = icon(SEND_PATH, 'send-icon');
send.append(sendIcon);
const bar = make('div');
hook(bar, 'bar');
const title = make('h2', { id: titleId }, strings.title);
Expand Down Expand Up @@ -646,7 +656,9 @@ function build(
const powered = make('a', { href: BUGSEE_URL, target: '_blank', rel: 'noopener noreferrer' });
hook(powered, 'powered');
powered.setAttribute('data-powered', '');
powered.append(logo, doc.createTextNode(strings.poweredBy));
const poweredText = make('span', {}, strings.poweredBy);
hook(poweredText, 'powered-text');
powered.append(logo, poweredText);
const footerRow = make('div');
hook(footerRow, 'footer-row');
footerRow.append(version, powered);
Expand Down
2 changes: 1 addition & 1 deletion packages/instrumentation-tests/test/report-dialog.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ describe('client.showReportDialog() — REAL Chromium end to end', () => {
radius: getComputedStyle(root.querySelector('.bugsee-reporting-dialog-card') as Element)
.borderRadius,
}));
expect(styled).toEqual({ styleElements: 0, adopted: 1, radius: '8px' });
expect(styled).toEqual({ styleElements: 0, adopted: 1, radius: '12px' });
expect(violations.map((v) => v.slice(0, 300))).toEqual([]);
await context.close();
} finally {
Expand Down
Loading