mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-11 01:36:35 +00:00
fix(web-shell): show workspace chip tooltip on narrow composer (#6958)
* fix(web-shell): show workspace chip tooltip on narrow composer The composer's workspace chip surfaced its full cwd only through a native `title` attribute, unlike the sibling git-branch and model chips which use a styled Radix tooltip. On a narrow (split-screen / mobile) composer the chip ellipsizes or collapses to an icon, so the workspace is discoverable only on hover — and a native `title` is inconsistent and never fires on touch. Give WorkspaceIndicator the same Radix tooltip as GitBranchIndicator (with the full cwd as content), completing the documented "mirrors GitBranchIndicator" intent. Its visually-hidden tooltip mirror also exposes the cwd to screen readers, which the native `title` did not do reliably. * test(web-shell): assert the workspace tooltip renders on hover Address review feedback: the WorkspaceIndicator tests checked the `data-web-shell-workspace-title` hook but never opened the tooltip, so a regression rendering the short name (or nothing) in the Radix `TooltipContent` would have gone unnoticed. Open the tooltip via a `pointermove` (jsdom has no `PointerEvent`; Radix opens on mouse move after `delayDuration`) and assert the portalled `[role="tooltip"]` shows the full cwd — and, in compact mode, assert the `workspaceChipCompact` icon-only class is actually applied. * test(web-shell): guard the compact chip before asserting on it Move the `if (!chip)` null guard above the assertions in the compact-mode test so a failure to render surfaces the descriptive "workspace chip was not rendered" error instead of an opaque `expect(undefined)` throw from the optional-chained access. Matches the first test in the file. --------- Co-authored-by: wenshao <wenshao@example.com>
This commit is contained in:
parent
7a1b182cd1
commit
38429bc100
3 changed files with 144 additions and 16 deletions
|
|
@ -232,7 +232,12 @@ describe('ChatEditor workspace toolbar integration', () => {
|
|||
});
|
||||
const chip = container.querySelector('[aria-label="Workspace: api"]');
|
||||
expect(chip).not.toBeNull();
|
||||
expect(chip?.getAttribute('title')).toBe('/work/api');
|
||||
// The full cwd is surfaced via the hover tooltip (mirroring the git branch
|
||||
// chip), not a native `title` attribute.
|
||||
expect(chip?.getAttribute('data-web-shell-workspace-title')).toBe(
|
||||
'/work/api',
|
||||
);
|
||||
expect(chip?.getAttribute('title')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-web-shell-workspace]'),
|
||||
).not.toBeNull();
|
||||
|
|
@ -247,7 +252,7 @@ describe('ChatEditor workspace toolbar integration', () => {
|
|||
expect(
|
||||
container
|
||||
.querySelector('[data-web-shell-workspace]')
|
||||
?.getAttribute('title'),
|
||||
?.getAttribute('data-web-shell-workspace-title'),
|
||||
).toBe('api');
|
||||
});
|
||||
|
||||
|
|
|
|||
108
packages/web-shell/client/components/WorkspaceIndicator.test.tsx
Normal file
108
packages/web-shell/client/components/WorkspaceIndicator.test.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { getTranslator } from '../i18n';
|
||||
import { WorkspaceIndicator } from './WorkspaceIndicator';
|
||||
|
||||
// Radix Tooltip only mounts its content while open, and opens on a mouse
|
||||
// `pointermove` over the trigger after `delayDuration`. jsdom has no
|
||||
// `PointerEvent`, so a plain bubbling `pointermove` Event stands in (Radix reads
|
||||
// `event.pointerType`, which is `undefined` here → treated as non-touch).
|
||||
function openTooltip(chip: HTMLElement) {
|
||||
act(() => {
|
||||
chip.dispatchEvent(new Event('pointermove', { bubbles: true }));
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
}
|
||||
|
||||
describe('WorkspaceIndicator', () => {
|
||||
it('reveals the full cwd via a hover tooltip, not a native title', () => {
|
||||
vi.useFakeTimers();
|
||||
const name = 'api';
|
||||
const title = '/work/services/a-very-long-web-shell-workspace-path/api';
|
||||
const ariaLabel = `Workspace: ${name}`;
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<WorkspaceIndicator name={name} title={title} ariaLabel={ariaLabel} />,
|
||||
);
|
||||
});
|
||||
|
||||
const chip = container.querySelector<HTMLElement>(
|
||||
`[aria-label="${ariaLabel}"]`,
|
||||
);
|
||||
if (!chip) throw new Error('workspace chip was not rendered');
|
||||
expect(chip.tagName).toBe('OUTPUT');
|
||||
expect(chip.textContent).toContain(name);
|
||||
// Non-interactive chip: no button, and no native `title` — the full cwd
|
||||
// rides in the Radix hover tooltip, matching the git branch chip.
|
||||
expect(container.querySelector('button')).toBeNull();
|
||||
expect(chip.getAttribute('title')).toBeNull();
|
||||
expect(chip.getAttribute('data-web-shell-workspace-title')).toBe(title);
|
||||
|
||||
// The headline behaviour: hovering renders the Radix tooltip with the full
|
||||
// cwd (guards against it rendering the short `name` or nothing at all).
|
||||
expect(document.querySelector('[role="tooltip"]')).toBeNull();
|
||||
openTooltip(chip);
|
||||
const tooltip = document.querySelector('[role="tooltip"]');
|
||||
expect(tooltip?.textContent).toBe(title);
|
||||
expect(tooltip?.textContent).not.toBe(name);
|
||||
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('keeps the full cwd discoverable when the name collapses in compact mode', () => {
|
||||
vi.useFakeTimers();
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<WorkspaceIndicator
|
||||
name="api"
|
||||
title="/work/api"
|
||||
ariaLabel="Workspace: api"
|
||||
compact
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const chip = container.querySelector<HTMLElement>(
|
||||
'[data-web-shell-workspace]',
|
||||
);
|
||||
if (!chip) throw new Error('workspace chip was not rendered');
|
||||
// Compact must actually apply the icon-only class...
|
||||
expect(chip.className).toContain('workspaceChipCompact');
|
||||
expect(chip.getAttribute('data-web-shell-workspace-title')).toBe(
|
||||
'/work/api',
|
||||
);
|
||||
|
||||
// ...and the tooltip must still reveal the cwd once the name is hidden, so a
|
||||
// narrow / mobile composer can tell which workspace the pane targets.
|
||||
openTooltip(chip);
|
||||
expect(document.querySelector('[role="tooltip"]')?.textContent).toBe(
|
||||
'/work/api',
|
||||
);
|
||||
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('localizes the accessible workspace label', () => {
|
||||
expect(getTranslator('en')('workspace.paneLabel', { name: 'api' })).toBe(
|
||||
'Workspace: api',
|
||||
);
|
||||
expect(getTranslator('zh-CN')('workspace.paneLabel', { name: 'api' })).toBe(
|
||||
'工作区:api',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,12 @@
|
|||
*/
|
||||
|
||||
import styles from './ChatEditor.module.css';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from './ui/tooltip';
|
||||
|
||||
function WorkspaceFolderIcon() {
|
||||
return (
|
||||
|
|
@ -24,7 +30,9 @@ function WorkspaceFolderIcon() {
|
|||
* session belongs to. Mirrors {@link GitBranchIndicator}; both sit in the
|
||||
* composer toolbar. Shown only on a multi-workspace daemon (the pane composer
|
||||
* opts into the `workspace` toolbar action) so it's clear which workspace a
|
||||
* message goes to.
|
||||
* message goes to. The full cwd stays in a hover tooltip — matching the git
|
||||
* branch chip — so it's still discoverable once the name ellipsizes or
|
||||
* collapses to an icon on a narrow (split-screen / mobile) composer.
|
||||
*/
|
||||
export function WorkspaceIndicator({
|
||||
name,
|
||||
|
|
@ -38,18 +46,25 @@ export function WorkspaceIndicator({
|
|||
compact?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<output
|
||||
className={`${styles.workspaceChip} ${
|
||||
compact ? styles.workspaceChipCompact : ''
|
||||
}`}
|
||||
title={title}
|
||||
aria-label={ariaLabel}
|
||||
data-web-shell-workspace
|
||||
>
|
||||
<span className={styles.workspaceChipIcon}>
|
||||
<WorkspaceFolderIcon />
|
||||
</span>
|
||||
<span className={styles.workspaceChipText}>{name}</span>
|
||||
</output>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<output
|
||||
className={`${styles.workspaceChip} ${
|
||||
compact ? styles.workspaceChipCompact : ''
|
||||
}`}
|
||||
aria-label={ariaLabel}
|
||||
data-web-shell-workspace
|
||||
data-web-shell-workspace-title={title}
|
||||
>
|
||||
<span className={styles.workspaceChipIcon}>
|
||||
<WorkspaceFolderIcon />
|
||||
</span>
|
||||
<span className={styles.workspaceChipText}>{name}</span>
|
||||
</output>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue