mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-22 23:25:28 +00:00
fix(web-shell): avoid duplicate inline tag tooltips (#6729)
* fix(web-shell): avoid duplicate inline tag tooltips * fix(web-shell): preserve inline tooltip fallback --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
This commit is contained in:
parent
01d406f1cc
commit
8a2be47ab7
2 changed files with 83 additions and 10 deletions
|
|
@ -122,6 +122,72 @@ describe('useComposerCore inline tags', () => {
|
|||
warn.mockRestore();
|
||||
});
|
||||
|
||||
it('uses a custom inline tooltip without a native title', async () => {
|
||||
await mount({
|
||||
composerInput: {
|
||||
tags: [{ id: 'orders', label: 'Table', value: 'orders' }],
|
||||
tagPlacement: 'inline',
|
||||
},
|
||||
renderComposerTagTooltip: () => 'Details',
|
||||
});
|
||||
|
||||
const tooltip = document.body.querySelector('[role="tooltip"]');
|
||||
expect(tooltip?.textContent).toBe('Details');
|
||||
expect(tooltip?.parentElement?.getAttribute('title')).toBeNull();
|
||||
expect(tooltip?.id).toBeTruthy();
|
||||
expect(tooltip?.parentElement?.getAttribute('aria-describedby')).toBe(
|
||||
tooltip?.id,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to a native title when attaching an inline tooltip fails', async () => {
|
||||
const error = new Error('append failed');
|
||||
const appendChild = HTMLElement.prototype.appendChild;
|
||||
let failingTooltip: HTMLElement | null = null;
|
||||
let readFailingChipTitle: (() => string) | null = null;
|
||||
let dispatchOnFailingChip: ((event: Event) => boolean) | null = null;
|
||||
const appendChildSpy = vi
|
||||
.spyOn(HTMLElement.prototype, 'appendChild')
|
||||
.mockImplementation(function (child) {
|
||||
if (
|
||||
child instanceof HTMLElement &&
|
||||
child.getAttribute('role') === 'tooltip'
|
||||
) {
|
||||
failingTooltip = child;
|
||||
readFailingChipTitle = () => this.title;
|
||||
dispatchOnFailingChip = (event) => this.dispatchEvent(event);
|
||||
throw error;
|
||||
}
|
||||
return appendChild.call(this, child);
|
||||
});
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {
|
||||
expect(readFailingChipTitle?.()).toBe('Details');
|
||||
});
|
||||
|
||||
try {
|
||||
await mount({
|
||||
composerInput: {
|
||||
tags: [{ id: 'orders', label: 'Table', value: 'orders' }],
|
||||
tagPlacement: 'inline',
|
||||
},
|
||||
renderComposerTagTooltip: () => 'Details',
|
||||
});
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'[WebShell] inline tag tooltip render failed',
|
||||
error,
|
||||
);
|
||||
const chip = document.body.querySelector('[title="Details"]');
|
||||
expect(chip).not.toBeNull();
|
||||
expect(document.body.querySelector('[role="tooltip"]')).toBeNull();
|
||||
dispatchOnFailingChip?.(new MouseEvent('mouseenter'));
|
||||
expect(failingTooltip?.style.display).toBe('none');
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
appendChildSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('guards inline mask icon sources', async () => {
|
||||
await mount({
|
||||
composerInput: {
|
||||
|
|
|
|||
|
|
@ -604,6 +604,8 @@ export const removeInlineTagEffect = StateEffect.define<{
|
|||
}>();
|
||||
export const clearInlineTagsEffect = StateEffect.define<void>();
|
||||
|
||||
let nextComposerTagTooltipId = 0;
|
||||
|
||||
class ComposerTagWidget extends WidgetType {
|
||||
private contentRoot: Root | null = null;
|
||||
private tooltipRoot: Root | null = null;
|
||||
|
|
@ -634,7 +636,6 @@ class ComposerTagWidget extends WidgetType {
|
|||
const publicTag = toPublicComposerTag(this.tag);
|
||||
chip.style.cssText =
|
||||
'position:relative;display:inline-flex;align-items:center;max-width:min(44ch,100%);min-height:20px;margin:0 0.25ch;border:1px solid var(--border);border-radius:4px;background:var(--secondary);color:var(--foreground);font-family:var(--font-mono,monospace);font-size:12px;line-height:1.2;vertical-align:baseline;';
|
||||
if (this.tag.tooltipText) chip.title = this.tag.tooltipText;
|
||||
if (this.tag.onClick) {
|
||||
chip.setAttribute('role', 'button');
|
||||
chip.tabIndex = 0;
|
||||
|
|
@ -760,6 +761,21 @@ class ComposerTagWidget extends WidgetType {
|
|||
tooltipElement.setAttribute('role', 'tooltip');
|
||||
tooltipElement.style.cssText =
|
||||
'position:absolute;z-index:calc(var(--web-shell-tooltip-z-index,1000) + 1);top:calc(100% + 6px);left:0;display:none;min-width:160px;max-width:min(320px,80vw);padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--background);box-shadow:0 8px 24px rgba(0,0,0,0.18);color:var(--foreground);font-family:var(--font-sans,system-ui,sans-serif);font-size:12px;line-height:1.5;white-space:normal;';
|
||||
try {
|
||||
this.tooltipRoot = createRoot(tooltipElement);
|
||||
this.tooltipRoot.render(tooltip);
|
||||
chip.appendChild(tooltipElement);
|
||||
tooltipElement.id = `composer-tag-tooltip-${++nextComposerTagTooltipId}`;
|
||||
chip.setAttribute('aria-describedby', tooltipElement.id);
|
||||
} catch (error) {
|
||||
this.tooltipRoot?.unmount();
|
||||
this.tooltipRoot = null;
|
||||
if (this.tag.tooltipText) {
|
||||
chip.title = this.tag.tooltipText;
|
||||
}
|
||||
console.warn('[WebShell] inline tag tooltip render failed', error);
|
||||
return;
|
||||
}
|
||||
const show = () => {
|
||||
tooltipElement.style.display = 'block';
|
||||
};
|
||||
|
|
@ -770,15 +786,6 @@ class ComposerTagWidget extends WidgetType {
|
|||
chip.addEventListener('mouseleave', hide);
|
||||
chip.addEventListener('focusin', show);
|
||||
chip.addEventListener('focusout', hide);
|
||||
try {
|
||||
this.tooltipRoot = createRoot(tooltipElement);
|
||||
this.tooltipRoot.render(tooltip);
|
||||
chip.appendChild(tooltipElement);
|
||||
} catch (error) {
|
||||
this.tooltipRoot?.unmount();
|
||||
this.tooltipRoot = null;
|
||||
console.warn('[WebShell] inline tag tooltip render failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
private appendRemoveButton(chip: HTMLElement, view: EditorView) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue