From ab86a7c5799a4c513778c71a49ceb7de909c2eef Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Mon, 13 Apr 2026 20:00:24 +0800 Subject: [PATCH] fix: preserve tooltip theme visibility overrides Tooltip theme merging was flattening nested style values and dropping root tooltip config such as mark and dimension visibility. This keeps style fields in spec.style, merges functional tooltip config at the root, and adds a focused regression for theme-driven tooltip visibility. Constraint: Tooltip themes must support both style fields and root spec fields Rejected: Fix only style merging | mark and dimension visibility would still be ignored Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep tooltip theme merge split between style keys and root spec keys Tested: Direct transformer probes for style nesting and mark visibility exclusion Tested: File-level TypeScript diagnostics on changed files Not-tested: Full package compile blocked by pre-existing continuous legend type errors --- .gitignore | 1 + .../__tests__/unit/theme/tooltip.test.ts | 85 +++++++++++++++++++ .../vchart/src/component/interface/theme.ts | 25 +++++- .../component/tooltip/tooltip-transformer.ts | 34 ++++++-- 4 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 packages/vchart/__tests__/unit/theme/tooltip.test.ts diff --git a/.gitignore b/.gitignore index f025af13fc..b25bde8401 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,4 @@ packages/vchart/__tests__/runtime/node/**.png *.tsbuildinfo .github/hooks/copilot-hooks.json +.omx/ diff --git a/packages/vchart/__tests__/unit/theme/tooltip.test.ts b/packages/vchart/__tests__/unit/theme/tooltip.test.ts new file mode 100644 index 0000000000..7d46340c60 --- /dev/null +++ b/packages/vchart/__tests__/unit/theme/tooltip.test.ts @@ -0,0 +1,85 @@ +import { mergeSpec } from '@visactor/vutils-extension'; +import { TooltipSpecTransformer } from '../../../src/component/tooltip/tooltip-transformer'; +import { ComponentTypeEnum } from '../../../src/component/interface/type'; +import { tooltip as builtInTooltipTheme } from '../../../src/theme/builtin/common/component/tooltip'; + +describe('tooltip theme transformer', () => { + const createTransformer = (tooltipTheme: Record) => + new TooltipSpecTransformer({ + type: ComponentTypeEnum.tooltip, + mode: 'node', + getTheme: (...keys: string[]) => { + if (keys[0] === 'component' && keys[1] === ComponentTypeEnum.tooltip) { + return mergeSpec({}, builtInTooltipTheme, tooltipTheme); + } + return undefined; + } + }); + + it('supports tooltip theme fields declared under component.tooltip.style', () => { + const transformer = createTransformer({ + style: { + panel: { + backgroundColor: '#123456' + }, + titleLabel: { + fill: '#abcdef', + fontSize: 18 + }, + keyLabel: { + fill: '#ff0000' + } + } + }); + + const { spec } = transformer.transformSpec({}, {}); + + expect(spec.style.panel.backgroundColor).toBe('#123456'); + expect(spec.style.titleLabel.fill).toBe('#abcdef'); + expect(spec.style.titleLabel.fontSize).toBe(18); + expect(spec.style.keyLabel.fill).toBe('#ff0000'); + expect(spec.style.style).toBeUndefined(); + }); + + it('keeps root tooltip theme fields at root instead of leaking them into style', () => { + const transformer = createTransformer({ + offset: { + x: 24, + y: 16 + }, + trigger: 'click', + transitionDuration: 0, + panel: { + backgroundColor: '#654321' + } + }); + + const { spec } = transformer.transformSpec({}, {}); + + expect(spec.offset).toEqual({ x: 24, y: 16 }); + expect(spec.trigger).toBe('click'); + expect(spec.transitionDuration).toBe(0); + expect(spec.style.panel.backgroundColor).toBe('#654321'); + expect(spec.style.offset).toBeUndefined(); + expect(spec.style.trigger).toBeUndefined(); + expect(spec.style.transitionDuration).toBeUndefined(); + }); + + it('supports active-type visibility declared in tooltip theme', () => { + const transformer = createTransformer({ + mark: { + visible: false + }, + dimension: { + visible: true + } + }); + + const { spec } = transformer.transformSpec({}, {}); + + expect(spec.mark.visible).toBe(false); + expect(spec.dimension.visible).toBe(true); + expect(spec.activeType).not.toContain('mark'); + expect(spec.activeType).toContain('dimension'); + }); +}); diff --git a/packages/vchart/src/component/interface/theme.ts b/packages/vchart/src/component/interface/theme.ts index f0472bf886..5cd6b06500 100644 --- a/packages/vchart/src/component/interface/theme.ts +++ b/packages/vchart/src/component/interface/theme.ts @@ -11,7 +11,7 @@ import type { IMarkLineTheme } from '../marker/mark-line/interface'; import type { IMarkPointTheme } from '../marker/mark-point/interface'; import type { IPlayerTheme } from '../player/interface'; import type { ITitleTheme } from '../title/interface'; -import type { ITooltipTheme } from '../tooltip/interface'; +import type { ITooltipSpec, ITooltipTheme } from '../tooltip/interface'; import type { ComponentTypeEnum } from './type'; import type { ITotalLabelTheme } from '../label/interface'; import type { IPoptipTheme } from '../poptip/interface'; @@ -80,7 +80,28 @@ export interface IComponentTheme { /** * tooltip 组件配置 */ - [ComponentTypeEnum.tooltip]?: ITooltipTheme; + [ComponentTypeEnum.tooltip]?: ITooltipTheme & + Partial< + Pick< + ITooltipSpec, + | 'visible' + | 'activeType' + | 'mark' + | 'dimension' + | 'group' + | 'trigger' + | 'triggerOff' + | 'showDelay' + | 'hideTimer' + | 'lockAfterClick' + | 'renderMode' + | 'confine' + | 'className' + | 'parentElement' + | 'enterable' + | 'throttleInterval' + > + >; /** * crosshair 配置 */ diff --git a/packages/vchart/src/component/tooltip/tooltip-transformer.ts b/packages/vchart/src/component/tooltip/tooltip-transformer.ts index 4c9052e418..bd3d43d486 100644 --- a/packages/vchart/src/component/tooltip/tooltip-transformer.ts +++ b/packages/vchart/src/component/tooltip/tooltip-transformer.ts @@ -6,6 +6,17 @@ import { TOOLTIP_EL_CLASS_NAME } from './constant'; import { getTooltipActualActiveType } from './utils/common'; import { mergeSpec } from '@visactor/vutils-extension'; +const TOOLTIP_STYLE_THEME_KEYS = [ + 'panel', + 'shape', + 'titleLabel', + 'keyLabel', + 'valueLabel', + 'spaceRow', + 'maxContentHeight', + 'align' +] as const; + export class TooltipSpecTransformer extends BaseComponentSpecTransformer { protected _shouldMergeThemeToSpec() { return false; @@ -13,17 +24,24 @@ export class TooltipSpecTransformer extends BaseComponentSpecTransformer { protected _initTheme(spec: any, chartSpec: any): { spec: any; theme: any } { const { spec: newSpec, theme } = super._initTheme(spec, chartSpec); + const themeStyle = mergeSpec( + {}, + ...TOOLTIP_STYLE_THEME_KEYS.map(key => (theme?.[key] !== undefined ? { [key]: theme[key] } : undefined)), + theme?.style + ); + const themeSpec = mergeSpec({}, theme); + + TOOLTIP_STYLE_THEME_KEYS.forEach(key => { + delete themeSpec[key]; + }); + delete themeSpec.style; - // 合并样式和配置 - newSpec.style = mergeSpec({}, this._theme, newSpec.style); - newSpec.offset = mergeSpec({}, theme.offset, spec.offset); - newSpec.transitionDuration = spec.transitionDuration ?? theme.transitionDuration; + const mergedSpec = mergeSpec({}, themeSpec, newSpec); - // 合并交互相关配置 - newSpec.trigger = spec.trigger ?? theme.trigger; - newSpec.triggerOff = spec.triggerOff ?? theme.triggerOff; + // 合并样式配置 + mergedSpec.style = mergeSpec({}, themeStyle, mergedSpec.style); - return { spec: newSpec, theme }; + return { spec: mergedSpec, theme }; } protected _transformSpecAfterMergingTheme(spec: any, chartSpec: any, chartSpecInfo?: IChartSpecInfo) {