fix(sheets-ui): align cell editor selection border (#7789)

This commit is contained in:
白熱
2026-09-29 20:25:16 +08:00
committed by GitHub
parent 2468486a93
commit 618f9df2cb
2 changed files with 27 additions and 59 deletions
@@ -14,9 +14,8 @@
* limitations under the License.
*/
import type { Nullable, Workbook } from '@univerjs/core';
import type { Workbook } from '@univerjs/core';
import type { KeyCode } from '@univerjs/ui';
import type { ICellEditorState } from '../../services/editor-bridge.service';
import {
DisposableCollection,
DOCS_NORMAL_EDITOR_UNIT_ID_KEY,
@@ -80,33 +79,6 @@ const EDITOR_DEFAULT_POSITION = {
const CELL_EDITOR_DARK_SURFACE_THEME_COLOR = 'gray.800';
const CELL_EDITOR_LIGHT_SURFACE_THEME_COLOR = 'gray.0';
interface ICellEditorHostBackgroundOptions {
darkMode?: boolean;
getColorFromTheme?: (color: string) => string | undefined;
}
/**
* @returns the host background color for the cell editor.
*/
function getCellEditorHostBackgroundColor(
editState: Nullable<Pick<ICellEditorState, 'documentLayoutObject'>>,
options: ICellEditorHostBackgroundOptions = {}
): string | undefined {
const cellFill = editState?.documentLayoutObject.fill;
if (cellFill && !isTransparentColor(cellFill)) {
return cellFill;
}
return options.getColorFromTheme?.(
options.darkMode ? CELL_EDITOR_DARK_SURFACE_THEME_COLOR : CELL_EDITOR_LIGHT_SURFACE_THEME_COLOR
);
}
function isTransparentColor(color: string) {
const normalizedColor = color.trim().toLowerCase().replace(/\s+/g, '');
return normalizedColor === 'transparent' || normalizedColor === 'rgba(0,0,0,0)';
}
export function shouldRefocusCellEditorAfterPointerDown(options: {
root: HTMLElement | null | undefined;
target: EventTarget | null | undefined;
@@ -243,6 +215,11 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
: null;
const activeSheet = useActiveWorksheet(workbook);
const darkMode = useObservable(themeService.darkMode$, themeService.darkMode);
const cellFill = editState?.documentLayoutObject.fill;
const normalizedFill = cellFill?.trim().toLowerCase().replace(/\s+/g, '');
const editorBackgroundColor = cellFill && normalizedFill !== 'transparent' && normalizedFill !== 'rgba(0,0,0,0)'
? cellFill
: themeService.getColorFromTheme(darkMode ? CELL_EDITOR_DARK_SURFACE_THEME_COLOR : CELL_EDITOR_LIGHT_SURFACE_THEME_COLOR);
const [showEditCellAddress, setShowEditCellAddress] = useState(false);
const editSheetName = editState ? workbook?.getSheetBySheetId(editState.sheetId)?.getName() : null;
const editCellAddress = editState
@@ -250,8 +227,7 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
? `'${quoteSheetName(editSheetName)}'!`
: ''}${numberToABC(editState.column)}${editState.row + 1}`
: null;
// The editor border uses the normal selection stroke, so the address label stays visually in sync with it.
const editorBorderColor = genNormalSelectionStyle(themeService).stroke;
const { stroke: editorBorderColor, strokeWidth: editorBorderWidth } = genNormalSelectionStyle(themeService);
const focusCoordinator = injector.has(ISheetEmbedRuntimeFocusCoordinator)
? injector.get(ISheetEmbedRuntimeFocusCoordinator)
: undefined;
@@ -630,8 +606,8 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
}, [contextService, disableAutoFocus, editorService, focusCoordinator, injector, instanceService, runtimeFocusState, visible?.visible]);
useEffect(() => {
const ownerWindow = rootRef.current?.ownerDocument.defaultView;
return () => {
const ownerWindow = rootRef.current?.ownerDocument.defaultView;
if (ownerWindow && pointerRefocusTimerRef.current != null) {
ownerWindow.clearTimeout(pointerRefocusTimerRef.current);
}
@@ -674,7 +650,7 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
const handleClickSideBar = useEvent(() => {
if (editorBridgeService.isVisible().visible) {
commandService.executeCommand(SetCellEditVisibleOperation.id, {
return commandService.executeCommand(SetCellEditVisibleOperation.id, {
visible: false,
eventType: DeviceInputEventType.PointerUp,
unitId: editState?.unitId,
@@ -687,7 +663,7 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
const keyCodeConfig = useKeyEventConfig(editState?.unitId);
const onMoveInEditor = useEvent((keycode: KeyCode, metaKey: MetaKeys) => {
commandService.executeCommand(SetCellEditVisibleArrowOperation.id, {
return commandService.executeCommand(SetCellEditVisibleArrowOperation.id, {
keycode,
visible: false,
eventType: DeviceInputEventType.Keyboard,
@@ -709,10 +685,7 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
height: state.height,
opacity: hidden ? 0 : undefined,
pointerEvents: hidden ? 'none' : undefined,
backgroundColor: getCellEditorHostBackgroundColor(editState, {
darkMode,
getColorFromTheme: themeService.getColorFromTheme.bind(themeService),
}),
backgroundColor: editorBackgroundColor,
}}
>
{visible?.visible && showEditCellAddress && editCellAddress && (
@@ -749,6 +722,7 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
resetSelectionOnBlur={false}
isSingle={false}
autoScrollbar={false}
borderless
onFormulaSelectingChange={(isSelecting: 0 | 1 | 2) => {
if (isSelecting) {
editorBridgeService.enableForceKeepVisible();
@@ -760,6 +734,21 @@ export function EditorContainer({ hidden = false }: IEditorContainerProps) {
canvasStyle={{ backgroundColor: 'transparent' }}
/>
)}
{visible?.visible && (
<div
aria-hidden
className="
univer-pointer-events-none univer-absolute univer-left-0 univer-top-0 univer-z-10
univer-box-border
"
style={{
// Selection strokes start at the cell edge and include its last pixel.
width: state.width + editorBorderWidth,
height: state.height + editorBorderWidth,
border: `${editorBorderWidth}px solid ${editorBorderColor}`,
}}
/>
)}
</div>
);
};
@@ -858,27 +858,6 @@ describe('EditorContainer embed focus lease', () => {
expect(cellEditorManagerService.setFocus).not.toHaveBeenCalled();
});
it('does not keep refocusing the embedded sheet cell editor on delayed timers after it becomes visible', async () => {
const { injector } = createTestBed({ docSelectionIsFocusing: false });
const setTimeoutSpy = vi.spyOn(window, 'setTimeout');
container = document.createElement('div');
container.setAttribute(EMBED_INTERACTION_BOUNDARY_OWNER_ATTRIBUTE, 'embed-1');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
renderEditorContainer(root!, injector);
await Promise.resolve();
});
expect(setTimeoutSpy).not.toHaveBeenCalledWith(expect.any(Function), 80);
expect(setTimeoutSpy).not.toHaveBeenCalledWith(expect.any(Function), 200);
expect(setTimeoutSpy).not.toHaveBeenCalledWith(expect.any(Function), 500);
expect(setTimeoutSpy).not.toHaveBeenCalledWith(expect.any(Function), 1000);
setTimeoutSpy.mockRestore();
});
it('restores focus to the internal cell editor after pointer down inside the editor canvas', async () => {
const { injector, docSelectionRenderService } = createTestBed({ docSelectionIsFocusing: false });
const hiddenEditor = document.createElement('div');