Skip to content

Instantly share code, notes, and snippets.

@clintandrewhall
Created May 6, 2025 13:52
Show Gist options
  • Save clintandrewhall/fc115e312ac27ae40f5f99fdc962adb9 to your computer and use it in GitHub Desktop.
Save clintandrewhall/fc115e312ac27ae40f5f99fdc962adb9 to your computer and use it in GitHub Desktop.
Hacked-up version of resizeable button for Kibana Chrome
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, {
useCallback,
useRef,
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
} from 'react';
import { EuiResizableButton, keys, useLatest } from '@elastic/eui';
import {
EuiResizableButtonKeyEvent,
KeyMoveDirection,
ResizeTrigger,
} from '@elastic/eui/src/components/resizable_container/types';
import {
setToolsPanelWidth,
useChromeDispatch,
useToolbarWidth,
useToolsPanelWidth,
} from '@kbn/core-chrome-state-internal';
import { styles } from './resizeable_button.styles';
export const isTouchEvent = (
event: MouseEvent | ReactMouseEvent | TouchEvent | ReactTouchEvent
): event is TouchEvent | ReactTouchEvent => typeof event === 'object' && 'targetTouches' in event;
export const getPosition = (
event: ReactMouseEvent | MouseEvent | ReactTouchEvent | TouchEvent
): number => {
const direction = 'clientX';
return isTouchEvent(event) ? event.targetTouches[0][direction] : event[direction];
};
interface Props {
/**
* Called when resizing starts
*/
onResizeStart?: (trigger: ResizeTrigger) => void;
/**
* Called when resizing ends
*/
onResizeEnd?: () => void;
}
export const ResizeableButton = ({ onResizeEnd, onResizeStart }: Props) => {
const toolbarWidth = useToolbarWidth();
const toolsPanelWidth = useToolsPanelWidth();
const dispatch = useChromeDispatch();
const onResizeEndRef = useLatest(onResizeEnd);
const onResizeStartRef = useLatest(onResizeStart);
const resizeContext = useRef<{
trigger?: ResizeTrigger;
keyMoveDirection?: KeyMoveDirection;
}>({});
const resizeEnd = useCallback(() => {
onResizeEndRef.current?.();
resizeContext.current = {};
}, [onResizeEndRef]);
const resizeStart = useCallback(
(trigger: ResizeTrigger, keyMoveDirection?: KeyMoveDirection) => {
// If another resize starts while the previous one is still in progress
// (e.g. user presses opposite arrow to change direction while the first
// is still held down, or user presses an arrow while dragging with the
// mouse), we want to signal the end of the previous resize first.
if (resizeContext.current.trigger) {
resizeEnd();
}
onResizeStartRef.current?.(trigger);
resizeContext.current = { trigger, keyMoveDirection };
},
[onResizeStartRef, resizeEnd]
);
const onMouseDown = useCallback(() => {
resizeStart('pointer');
// Window event listeners instead of React events are used to continue
// detecting movement even if the user's mouse leaves the container
const onMouseMove = (e: MouseEvent | TouchEvent) => {
const pos = getPosition(e);
// This shouldn't be here, should be a handler?
dispatch(setToolsPanelWidth(document.body.clientWidth - pos - toolbarWidth));
};
const onMouseUp = () => {
if (resizeContext.current.trigger === 'pointer') {
resizeEnd();
}
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
window.removeEventListener('touchmove', onMouseMove);
window.removeEventListener('touchend', onMouseUp);
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
window.addEventListener('touchmove', onMouseMove);
window.addEventListener('touchend', onMouseUp);
}, [resizeStart, resizeEnd, toolbarWidth, dispatch]);
const getKeyMoveDirection = useCallback((key: string) => {
let dir: KeyMoveDirection | null = null;
if (key === keys.ARROW_LEFT) {
dir = 'backward';
} else if (key === keys.ARROW_RIGHT) {
dir = 'forward';
}
return dir;
}, []);
const onKeyDown = useCallback(
(event: EuiResizableButtonKeyEvent) => {
const { key } = event;
const dir = getKeyMoveDirection(key);
if (dir) {
if (!event.repeat) {
// I'm not certain this would be the handler I'd expect.
resizeStart('key', dir);
}
event.preventDefault();
// These shouldn't be here, should be handlers?
if (dir === 'backward') {
dispatch(setToolsPanelWidth(toolsPanelWidth + 10));
} else {
dispatch(setToolsPanelWidth(toolsPanelWidth - 10));
}
}
},
[getKeyMoveDirection, resizeStart, toolsPanelWidth, dispatch]
);
const onKeyUp = useCallback(
({ key }: EuiResizableButtonKeyEvent) => {
// We only want to signal the end of a resize if the key that was released
// is the same as the one that started the resize. This prevents the end
// of a resize if the user presses one arrow key, then presses the opposite
// arrow key to change direction, then releases the first arrow key.
if (
resizeContext.current.trigger === 'key' &&
resizeContext.current.keyMoveDirection === getKeyMoveDirection(key)
) {
resizeEnd();
}
},
[getKeyMoveDirection, resizeEnd]
);
const onBlur = useCallback(() => {
if (resizeContext.current.trigger === 'key') {
resizeEnd();
}
}, [resizeEnd]);
return (
<EuiResizableButton
css={styles.root}
isHorizontal={true}
{...{
onKeyDown,
onKeyUp,
onMouseDown,
onTouchStart: onMouseDown,
onBlur,
}}
/>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment