From 99eb8778289370e026f51d4d55a908dc281154ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Thu, 5 Feb 2026 22:13:14 +0800 Subject: [PATCH 1/6] test(modal): add mouseDown event before click in test (#56874) * test(modal): add mouseDown event before click in test Co-Authored-By: Claude Opus 4.5 * test(modal): add mouseDown event before click in confirm test Co-Authored-By: Claude Opus 4.5 * test(modal): improve confirm test event firing and timer management - Add mouseDown event before click for mask interaction - Use fireEvent consistently instead of direct click() - Properly setup and cleanup fake timers Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- components/modal/__tests__/confirm.test.tsx | 31 +++++++++++++++++---- components/modal/__tests__/hook.test.tsx | 2 ++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/components/modal/__tests__/confirm.test.tsx b/components/modal/__tests__/confirm.test.tsx index 50074e7fae..f99c844a24 100644 --- a/components/modal/__tests__/confirm.test.tsx +++ b/components/modal/__tests__/confirm.test.tsx @@ -678,6 +678,8 @@ describe('Modal.confirm triggers callbacks correctly', () => { describe('the callback close should be a method when onCancel has a close parameter', () => { (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`click the close icon to trigger ${type} onCancel`, async () => { + jest.useFakeTimers(); + const mock = jest.fn(); Modal[type]?.({ @@ -688,17 +690,21 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); - $$('.ant-modal-close')[0].click(); + fireEvent.click($$('.ant-modal-close')[0]); await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); + + jest.useRealTimers(); }); }); (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`press ESC to trigger ${type} onCancel`, async () => { + jest.useFakeTimers(); + const mock = jest.fn(); Modal[type]?.({ @@ -709,17 +715,21 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); - fireEvent.keyDown(window, { key: 'Escape' }); + fireEvent.keyDown($$(`.ant-modal-confirm-${type}`)[0], { key: 'Escape' }); await waitFakeTimer(0); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); + + jest.useRealTimers(); }); }); (['confirm', 'info', 'success', 'warning', 'error'] as const).forEach((type) => { it(`click the mask to trigger ${type} onCancel`, async () => { + jest.useFakeTimers(); + const mock = jest.fn(); Modal[type]?.({ @@ -732,17 +742,22 @@ describe('Modal.confirm triggers callbacks correctly', () => { expect($$('.ant-modal-mask')).toHaveLength(1); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(1); - $$('.ant-modal-wrap')[0].click(); + fireEvent.mouseDown($$('.ant-modal-wrap')[0]); + fireEvent.click($$('.ant-modal-wrap')[0]); await waitFakeTimer(); expect($$(`.ant-modal-confirm-${type}`)).toHaveLength(0); expect(mock).toHaveBeenCalledWith(expect.any(Function)); + + jest.useRealTimers(); }); }); }); it('confirm modal click Cancel button close callback is a function', async () => { + jest.useFakeTimers(); + const mock = jest.fn(); Modal.confirm({ @@ -751,13 +766,17 @@ describe('Modal.confirm triggers callbacks correctly', () => { await waitFakeTimer(); - $$('.ant-modal-confirm-btns > .ant-btn')[0].click(); + fireEvent.click($$('.ant-modal-confirm-btns > .ant-btn')[0]); await waitFakeTimer(); expect(mock).toHaveBeenCalledWith(expect.any(Function)); + + jest.useRealTimers(); }); it('close can close modal when onCancel has a close parameter', async () => { + jest.useFakeTimers(); + Modal.confirm({ onCancel: (close) => close(), }); @@ -766,10 +785,12 @@ describe('Modal.confirm triggers callbacks correctly', () => { expect($$('.ant-modal-confirm-confirm')).toHaveLength(1); - $$('.ant-modal-confirm-btns > .ant-btn')[0].click(); + fireEvent.click($$('.ant-modal-confirm-btns > .ant-btn')[0]); await waitFakeTimer(); expect($$('.ant-modal-confirm-confirm')).toHaveLength(0); + + jest.useRealTimers(); }); // https://github.com/ant-design/ant-design/issues/37461 diff --git a/components/modal/__tests__/hook.test.tsx b/components/modal/__tests__/hook.test.tsx index 4a738fa45f..9fd221e8e1 100644 --- a/components/modal/__tests__/hook.test.tsx +++ b/components/modal/__tests__/hook.test.tsx @@ -186,6 +186,7 @@ describe('Modal.hook', () => { expect(cancelCount).toEqual(1); // click cancel btn, trigger onCancel fireEvent.click(container.querySelectorAll('.open-hook-modal-btn')[0]); + fireEvent.mouseDown(document.body.querySelectorAll('.ant-modal-wrap')[0]); fireEvent.click(document.body.querySelectorAll('.ant-modal-wrap')[0]); expect(cancelCount).toEqual(2); // click modal wrapper, trigger onCancel }); @@ -322,6 +323,7 @@ describe('Modal.hook', () => { expect(document.body.querySelectorAll('.ant-modal-confirm-confirm')).toHaveLength(1); // Click mask to close + fireEvent.mouseDown(document.body.querySelectorAll('.ant-modal-wrap')[0]); fireEvent.click(document.body.querySelectorAll('.ant-modal-wrap')[0]); await waitFakeTimer(); From bbb0683ab6aed27519a549d112df4c3d6e332521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 6 Feb 2026 11:10:41 +0800 Subject: [PATCH 2/6] fix(button): add theme-aware preset color hover/active tokens (#56872) * fix(button): add theme-aware preset color hover/active tokens Add ${colorKey}Hover and ${colorKey}Active tokens that swap values based on dark/light mode for improved contrast and user experience. Co-Authored-By: Claude Opus 4.5 * fix: add preset color hover/active tokens for consistent button interaction - Add xxxHover and xxxActive tokens for preset colors in genColorMapToken - Override these tokens in dark mode to swap hover/active values - Update ButtonToken type to include PresetColorHoverActiveMap - Update button variant styles to use new hover/active tokens - Fix #56656: button hover/active state inconsistency in dark mode * test: add @csstools to compileModules Co-Authored-By: Claude Opus 4.5 * test: support .mjs files in Jest configuration - Updated .jest.js transform pattern to include .mjs files - Simplified .jest.node.js transform patterns - Added jest-mjs-transformer.js for babel-jest mjs handling Co-Authored-By: Claude Opus 4.5 * test: remove unused jest-mjs-transformer.js - Removed jest-mjs-transformer.js as it's no longer used - .mjs files are now handled by the updated transform patterns in Jest configs * test: add .mjs support to .jest.image.js - Updated .jest.image.js transform pattern to include .mjs files - fixes image test failures due to ES module parsing errors --------- Co-authored-by: Claude Opus 4.5 --- .jest.image.js | 4 +--- .jest.js | 3 ++- .jest.node.js | 4 +--- components/button/style/token.ts | 12 ++++++++++-- components/button/style/variant.ts | 4 ++-- components/theme/themes/dark/index.ts | 16 ++++++++++++++++ .../theme/themes/shared/genColorMapToken.ts | 13 +++++++++++++ 7 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.jest.image.js b/.jest.image.js index 78191c45d4..8038d666d8 100644 --- a/.jest.image.js +++ b/.jest.image.js @@ -1,13 +1,11 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest'); -// jest config for image snapshots module.exports = { setupFiles: ['./tests/setup.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'md'], moduleNameMapper, transform: { - '\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', - '\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', + '^.+\\.(ts|tsx|js|mjs)$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', '\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor', '\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor', }, diff --git a/.jest.js b/.jest.js index 3a431c0f34..20a17166c0 100644 --- a/.jest.js +++ b/.jest.js @@ -11,6 +11,7 @@ const compileModules = [ 'parse5', '@exodus', 'jsdom', + '@csstools', ]; // cnpm use `_` as prefix @@ -62,7 +63,7 @@ module.exports = { ], transform: { '\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', - '\\.(m?)js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', + '\\.(m?)js(m)?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', '\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor', '\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor', }, diff --git a/.jest.node.js b/.jest.node.js index e4631d406e..aad21544be 100644 --- a/.jest.node.js +++ b/.jest.node.js @@ -1,14 +1,12 @@ const { moduleNameMapper, transformIgnorePatterns } = require('./.jest'); -// jest config for server render environment module.exports = { setupFiles: ['./tests/setup.ts'], setupFilesAfterEnv: ['./tests/setupAfterEnv.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'md'], moduleNameMapper, transform: { - '\\.tsx?$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', - '\\.js$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', + '^.+\\.(ts|tsx|js|mjs)$': './node_modules/@ant-design/tools/lib/jest/codePreprocessor', '\\.md$': './node_modules/@ant-design/tools/lib/jest/demoPreprocessor', '\\.(jpg|png|gif|svg)$': './node_modules/@ant-design/tools/lib/jest/imagePreprocessor', }, diff --git a/components/button/style/token.ts b/components/button/style/token.ts index dedb914ee9..e12a92e204 100644 --- a/components/button/style/token.ts +++ b/components/button/style/token.ts @@ -4,9 +4,9 @@ import { unit } from '@ant-design/cssinjs'; import { AggregationColor } from '../../color-picker/color'; import { isBright } from '../../color-picker/components/ColorPresets'; +import { PresetColors } from '../../theme/interface'; import type { FullToken, GenStyleFn, GetDefaultToken, PresetColorKey } from '../../theme/internal'; import { getLineHeight, mergeToken } from '../../theme/internal'; -import { PresetColors } from '../../theme/interface'; import getAlphaColor from '../../theme/util/getAlphaColor'; /** Component only token. Which will handle additional calculation of alias token */ @@ -247,6 +247,10 @@ type ShadowColorMap = { [Key in `${PresetColorKey}ShadowColor`]: string; }; +type PresetColorHoverActiveMap = { + [Key in `${PresetColorKey}Hover` | `${PresetColorKey}Active`]: string; +}; + type GroupToken = { /** * @desc 按钮组边框颜色 @@ -257,7 +261,11 @@ type GroupToken = { groupBorderColor: string; }; -export interface ButtonToken extends FullToken<'Button'>, ShadowColorMap, GroupToken { +export interface ButtonToken + extends FullToken<'Button'>, + ShadowColorMap, + PresetColorHoverActiveMap, + GroupToken { /** * @desc 按钮横向内边距 * @descEN Horizontal padding of button diff --git a/components/button/style/variant.ts b/components/button/style/variant.ts index ecfc8c9225..edb6cbb68c 100644 --- a/components/button/style/variant.ts +++ b/components/button/style/variant.ts @@ -268,10 +268,10 @@ const genVariantStyle: GenerateStyle = (token) => { PresetColors.map((colorKey) => { const darkColor = token[`${colorKey}6`]; const lightColor = token[`${colorKey}1`]; - const hoverColor = token[`${colorKey}5`]; + const hoverColor = token[`${colorKey}Hover`]; const lightHoverColor = token[`${colorKey}2`]; const lightActiveColor = token[`${colorKey}3`]; - const activeColor = token[`${colorKey}7`]; + const activeColor = token[`${colorKey}Active`]; const shadowColor = token[`${colorKey}ShadowColor`]; diff --git a/components/theme/themes/dark/index.ts b/components/theme/themes/dark/index.ts index 746905a318..56d8441bcb 100644 --- a/components/theme/themes/dark/index.ts +++ b/components/theme/themes/dark/index.ts @@ -2,6 +2,7 @@ import { generate } from '@ant-design/colors'; import type { DerivativeFunc } from '@ant-design/cssinjs'; import type { MapToken, PresetColorType, SeedToken } from '../../interface'; +import { PresetColors } from '../../interface/presetColors'; import defaultAlgorithm from '../default'; import { defaultPresetColors } from '../seed'; import genColorMapToken from '../shared/genColorMapToken'; @@ -29,6 +30,19 @@ const derivative: DerivativeFunc = (token, mapToken) => { generateNeutralColorPalettes, }); + const presetColorHoverActiveTokens = PresetColors.reduce>( + (prev, colorKey) => { + const colorBase = token[colorKey as keyof PresetColorType]; + if (colorBase) { + const colorPalette = generateColorPalettes(colorBase); + prev[`${colorKey}Hover`] = colorPalette[7]; + prev[`${colorKey}Active`] = colorPalette[5]; + } + return prev; + }, + {}, + ); + return { ...mergedMapToken, @@ -38,6 +52,8 @@ const derivative: DerivativeFunc = (token, mapToken) => { // Colors ...colorMapToken, + ...presetColorHoverActiveTokens, + // Customize selected item background color // https://github.com/ant-design/ant-design/issues/30524#issuecomment-871961867 colorPrimaryBg: colorMapToken.colorPrimaryBorder, diff --git a/components/theme/themes/shared/genColorMapToken.ts b/components/theme/themes/shared/genColorMapToken.ts index bfc96efb3a..aef704624e 100644 --- a/components/theme/themes/shared/genColorMapToken.ts +++ b/components/theme/themes/shared/genColorMapToken.ts @@ -1,6 +1,7 @@ import { FastColor } from '@ant-design/fast-color'; import type { ColorMapToken, SeedToken } from '../../interface'; +import { PresetColors } from '../../interface/presetColors'; import type { GenerateColorMap, GenerateNeutralColorMap } from '../ColorMap'; interface PaletteGenerators { @@ -37,6 +38,16 @@ export default function genColorMapToken( .mix(new FastColor(errorColors[3]), 50) .toHexString(); + const presetColorTokens: Record = {}; + PresetColors.forEach((colorKey) => { + const colorBase = seed[colorKey]; + if (colorBase) { + const colorPalette = generateColorPalettes(colorBase); + presetColorTokens[`${colorKey}Hover`] = colorPalette[5]; + presetColorTokens[`${colorKey}Active`] = colorPalette[7]; + } + }); + return { ...neutralColors, @@ -101,6 +112,8 @@ export default function genColorMapToken( colorLink: linkColors[6], colorLinkActive: linkColors[7], + ...presetColorTokens, + colorBgMask: new FastColor('#000').setA(0.45).toRgbString(), colorWhite: '#fff', }; From 8a16c49f5cbd6de39b71a2f6239cb0d6b08d54f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:36:50 +0800 Subject: [PATCH 3/6] chore: upgrade deps (#56877) Co-authored-by: afc163 <507615+afc163@users.noreply.github.com> Co-authored-by: thinkasany <480968828@qq.com> --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c3739cb56d..ba86dedf44 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "@rc-component/checkbox": "~1.0.1", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.0.3", - "@rc-component/dialog": "~1.8.2", + "@rc-component/dialog": "~1.8.3", "@rc-component/drawer": "~1.4.1", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.6.2", @@ -151,7 +151,7 @@ "@rc-component/tree-select": "~1.6.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", - "@rc-component/util": "^1.8.1", + "@rc-component/util": "^1.8.2", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", From 2950b7da98a2b3f7b6ddfd402fdd22a695608326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Fri, 6 Feb 2026 14:37:55 +0800 Subject: [PATCH 4/6] fix(tooltip): rename arrow-offset-horizontal to arrow-offset-x CSS variable to fix transformOrigin (#56887) The variable name was inconsistent and caused transformOrigin to not properly reference the arrow position for animations. Co-authored-by: Claude Opus 4.6 --- components/popover/style/index.ts | 2 +- components/style/placementArrow.ts | 8 ++++---- components/tooltip/style/index.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/popover/style/index.ts b/components/popover/style/index.ts index b3a2a00b3f..5a363236f5 100644 --- a/components/popover/style/index.ts +++ b/components/popover/style/index.ts @@ -108,7 +108,7 @@ const genBaseStyle: GenerateStyle = (token) => { userSelect: 'text', // When use `autoArrow`, origin will follow the arrow position - [varName('valid-offset-x')]: varRef('arrow-offset-horizontal', 'var(--arrow-x)'), + [varName('valid-offset-x')]: varRef('arrow-offset-x', 'var(--arrow-x)'), transformOrigin: [ varRef('valid-offset-x', FALL_BACK_ORIGIN), `var(--arrow-y, ${FALL_BACK_ORIGIN})`, diff --git a/components/style/placementArrow.ts b/components/style/placementArrow.ts index cf3a81f30c..ed8e22f871 100644 --- a/components/style/placementArrow.ts +++ b/components/style/placementArrow.ts @@ -106,7 +106,7 @@ const getArrowStyle = < }, '&-placement-topLeft': { - [varName('arrow-offset-horizontal')]: arrowOffsetHorizontal, + [varName('arrow-offset-x')]: arrowOffsetHorizontal, [`> ${componentCls}-arrow`]: { left: { @@ -117,7 +117,7 @@ const getArrowStyle = < }, '&-placement-topRight': { - [varName('arrow-offset-horizontal')]: `calc(100% - ${unit(arrowOffsetHorizontal)})`, + [varName('arrow-offset-x')]: `calc(100% - ${unit(arrowOffsetHorizontal)})`, [`> ${componentCls}-arrow`]: { right: { @@ -148,7 +148,7 @@ const getArrowStyle = < }, '&-placement-bottomLeft': { - [varName('arrow-offset-horizontal')]: arrowOffsetHorizontal, + [varName('arrow-offset-x')]: arrowOffsetHorizontal, [`> ${componentCls}-arrow`]: { left: { @@ -159,7 +159,7 @@ const getArrowStyle = < }, '&-placement-bottomRight': { - [varName('arrow-offset-horizontal')]: `calc(100% - ${unit(arrowOffsetHorizontal)})`, + [varName('arrow-offset-x')]: `calc(100% - ${unit(arrowOffsetHorizontal)})`, [`> ${componentCls}-arrow`]: { right: { diff --git a/components/tooltip/style/index.ts b/components/tooltip/style/index.ts index 647b757f21..6511fad009 100644 --- a/components/tooltip/style/index.ts +++ b/components/tooltip/style/index.ts @@ -83,7 +83,7 @@ const genTooltipStyle: GenerateStyle = (token) => { const sharedTransformOrigin: CSSObject = { // When use `autoArrow`, origin will follow the arrow position - [varName('valid-offset-x')]: varRef('arrow-offset-horizontal', 'var(--arrow-x)'), + [varName('valid-offset-x')]: varRef('arrow-offset-x', 'var(--arrow-x)'), transformOrigin: [ varRef('valid-offset-x', FALL_BACK_ORIGIN), `var(--arrow-y, ${FALL_BACK_ORIGIN})`, From b08961831d8d20ee5b394bd6dd4d8901133952ab Mon Sep 17 00:00:00 2001 From: thinkasany <480968828@qq.com> Date: Fri, 6 Feb 2026 14:43:47 +0800 Subject: [PATCH 5/6] chore: bump @rc-component/dialog --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a187efb64..41cbf6ba1e 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.0", - "@rc-component/dialog": "~1.8.2", + "@rc-component/dialog": "~1.8.3", "@rc-component/drawer": "~1.4.1", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.6.2", From ce4c9399109b54d602a0776acf53397b00b42ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 6 Feb 2026 16:56:00 +0800 Subject: [PATCH 6/6] test(modal): add mouseDown event before click to fix mask click tests --- components/modal/__tests__/Modal.test.tsx | 1 + components/modal/__tests__/hook.test.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/components/modal/__tests__/Modal.test.tsx b/components/modal/__tests__/Modal.test.tsx index 28b25aeb8a..8240715c9f 100644 --- a/components/modal/__tests__/Modal.test.tsx +++ b/components/modal/__tests__/Modal.test.tsx @@ -297,6 +297,7 @@ describe('Modal', () => { await waitFakeTimer(500); }); const modalWrap = document.body.querySelectorAll('.ant-modal-wrap')[0]; + fireEvent.mouseDown(modalWrap!); fireEvent.click(modalWrap!); await act(async () => { await waitFakeTimer(500); diff --git a/components/modal/__tests__/hook.test.tsx b/components/modal/__tests__/hook.test.tsx index 585387feb6..e9b9a4cfdf 100644 --- a/components/modal/__tests__/hook.test.tsx +++ b/components/modal/__tests__/hook.test.tsx @@ -225,6 +225,7 @@ describe('Modal.hook', () => { expect(cancelCount).toEqual(1); // click cancel btn, trigger onCancel fireEvent.click(container.querySelectorAll('.open-hook-modal-btn')[0]); + fireEvent.mouseDown(document.body.querySelectorAll('.ant-modal-wrap')[0]); fireEvent.click(document.body.querySelectorAll('.ant-modal-wrap')[0]); expect(cancelCount).toEqual(2); // click modal wrapper, trigger onCancel });