diff --git a/src-docs/src/views/resizable_container/resizable_container_callbacks.tsx b/src-docs/src/views/resizable_container/resizable_container_callbacks.tsx
new file mode 100644
index 00000000000..2877e146b4c
--- /dev/null
+++ b/src-docs/src/views/resizable_container/resizable_container_callbacks.tsx
@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import {
+ EuiText,
+ EuiResizableContainer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiStat,
+ EuiPanel,
+} from '../../../../src/components';
+// @ts-ignore - faker does not have type declarations
+import { fake } from 'faker';
+import { useGeneratedHtmlId } from '../../../../src/services';
+
+const text = (
+ <>
+
{fake('{{lorem.paragraphs}}')}
+ {fake('{{lorem.paragraphs}}')}
+ {fake('{{lorem.paragraphs}}')}
+ >
+);
+
+export default () => {
+ const firstPanelId = useGeneratedHtmlId({ prefix: 'firstPanel' });
+ const secondPanelId = useGeneratedHtmlId({ prefix: 'secondPanel' });
+ const [resizeTrigger, setResizeTrigger] = useState<'pointer' | 'key'>();
+ const [sizes, setSizes] = useState({
+ [firstPanelId]: 50,
+ [secondPanelId]: 50,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setSizes((prevSizes) => ({ ...prevSizes, ...newSizes }));
+ }}
+ onResizeStart={(trigger) => setResizeTrigger(trigger)}
+ onResizeEnd={() => setResizeTrigger(undefined)}
+ >
+ {(EuiResizablePanel, EuiResizableButton) => (
+ <>
+
+
+ {text}
+ Hello world
+
+
+
+
+
+
+ {text}
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/src-docs/src/views/resizable_container/resizable_container_example.js b/src-docs/src/views/resizable_container/resizable_container_example.js
index 51646080d83..8f3e2d6f753 100644
--- a/src-docs/src/views/resizable_container/resizable_container_example.js
+++ b/src-docs/src/views/resizable_container/resizable_container_example.js
@@ -23,6 +23,7 @@ import { PanelModeType } from '!!prop-loader!../../../../src/components/resizabl
import ResizableContainerBasic from './resizable_container_basic';
import ResizableContainerVertical from './resizable_container_vertical';
import ResizableContainerResetValues from './resizable_container_reset_values';
+import ResizableContainerCallbacks from './resizable_container_callbacks';
import ResizablePanels from './resizable_panels';
import ResizablePanelCollapsible from './resizable_panel_collapsible';
import ResizablePanelCollapsibleResponsive from './resizable_panel_collapsible_responsive';
@@ -32,6 +33,7 @@ import ResizablePanelCollapsibleExt from './resizable_panel_collapsible_external
const ResizableContainerSource = require('!!raw-loader!./resizable_container_basic');
const ResizableContainerVerticalSource = require('!!raw-loader!./resizable_container_vertical');
const ResizableContainerResetValuesSource = require('!!raw-loader!./resizable_container_reset_values');
+const ResizableContainerCallbacksSource = require('!!raw-loader!./resizable_container_callbacks');
const ResizablePanelsSource = require('!!raw-loader!./resizable_panels');
const ResizablePanelCollapsibleSource = require('!!raw-loader!./resizable_panel_collapsible');
const ResizablePanelCollapsibleResponsiveSource = require('!!raw-loader!./resizable_panel_collapsible_responsive');
@@ -80,6 +82,29 @@ const verticalSnippet = `
)}
`;
+const callbacksSnippet = ` console.log('onResizeStart', trigger)}
+ onResizeEnd={() => console.log('onResizeEnd')}
+>
+ {(EuiResizablePanel, EuiResizableButton) => (
+ <>
+
+
+ {text}
+
+
+
+
+
+
+
+ {text}
+
+
+ >
+ )}
+`;
+
const collapsibleSnippet = `
{(EuiResizablePanel, EuiResizableButton) => (
<>
@@ -313,6 +338,30 @@ export const ResizableContainerExample = {
demo: ,
snippet: verticalSnippet,
},
+ {
+ source: [
+ {
+ type: GuideSectionTypes.TSX,
+ code: ResizableContainerCallbacksSource,
+ },
+ ],
+ title: 'Resizable container callbacks',
+ text: (
+ <>
+
+ EuiResizableContainer supports{' '}
+ onResizeStart and onResizeEnd{' '}
+ callback props to listen for when resizing starts and ends. The{' '}
+ onResizeStart callback is passed a{' '}
+ {"trigger: 'pointer' | 'key'"} parameter to
+ determine which user action triggered the resize.
+
+ >
+ ),
+ props: { EuiResizableContainer, EuiResizablePanel, EuiResizableButton },
+ demo: ,
+ snippet: callbacksSnippet,
+ },
{
source: [
{
diff --git a/src/components/resizable_container/resizable_button.tsx b/src/components/resizable_container/resizable_button.tsx
index 80a9676d3fc..4d1fa085fe2 100644
--- a/src/components/resizable_container/resizable_button.tsx
+++ b/src/components/resizable_container/resizable_button.tsx
@@ -23,11 +23,12 @@ import { useEuiResizableContainerContext } from './context';
import {
EuiResizableButtonController,
EuiResizableButtonMouseEvent,
- EuiResizableButtonKeyDownEvent,
+ EuiResizableButtonKeyEvent,
} from './types';
interface EuiResizableButtonControls {
- onKeyDown: (eve: EuiResizableButtonKeyDownEvent) => void;
+ onKeyDown: (eve: EuiResizableButtonKeyEvent) => void;
+ onKeyUp: (eve: EuiResizableButtonKeyEvent) => void;
onMouseDown: (eve: EuiResizableButtonMouseEvent) => void;
onTouchStart: (eve: EuiResizableButtonMouseEvent) => void;
onFocus: (id: string) => void;
diff --git a/src/components/resizable_container/resizable_container.test.tsx b/src/components/resizable_container/resizable_container.test.tsx
index 9dc8971e4da..7f7fe34f0db 100644
--- a/src/components/resizable_container/resizable_container.test.tsx
+++ b/src/components/resizable_container/resizable_container.test.tsx
@@ -7,10 +7,11 @@
*/
import React from 'react';
-import { render } from 'enzyme';
-import { requiredProps } from '../../test';
+import { mount, render } from 'enzyme';
+import { findTestSubject, requiredProps } from '../../test';
import { EuiResizableContainer } from './resizable_container';
+import { keys } from '../../services';
describe('EuiResizableContainer', () => {
test('is rendered', () => {
@@ -170,4 +171,168 @@ describe('EuiResizableContainer', () => {
expect(component).toMatchSnapshot();
});
+
+ describe('on resize callbacks', () => {
+ const mountWithCallbacks = ({
+ direction,
+ }: {
+ direction?: 'vertical' | 'horizontal';
+ } = {}) => {
+ const onResizeStart = jest.fn();
+ const onResizeEnd = jest.fn();
+ const component = mount(
+
+ {(EuiResizablePanel, EuiResizableButton) => (
+ <>
+ Testing
+
+ 123
+ >
+ )}
+
+ );
+ const container = findTestSubject(component, 'euiResizableContainer');
+ const button = findTestSubject(component, 'euiResizableButton');
+ return { container, button, onResizeStart, onResizeEnd };
+ };
+
+ test('onResizeStart and onResizeEnd are called for pointer events', () => {
+ const {
+ container,
+ button,
+ onResizeStart,
+ onResizeEnd,
+ } = mountWithCallbacks();
+ button.simulate('mousedown', {
+ pageX: 0,
+ pageY: 0,
+ clientX: 0,
+ clientY: 0,
+ });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('pointer');
+ container.simulate('mouseup');
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ button.simulate('mousedown', {
+ pageX: 0,
+ pageY: 0,
+ clientX: 0,
+ clientY: 0,
+ });
+ expect(onResizeStart).toHaveBeenCalledTimes(2);
+ expect(onResizeStart).toHaveBeenLastCalledWith('pointer');
+ container.simulate('mouseleave');
+ expect(onResizeEnd).toHaveBeenCalledTimes(2);
+ button.simulate('touchstart', {
+ touches: [
+ {
+ clientX: 0,
+ clientY: 0,
+ },
+ ],
+ });
+ expect(onResizeStart).toHaveBeenCalledTimes(3);
+ expect(onResizeStart).toHaveBeenLastCalledWith('pointer');
+ container.simulate('touchend');
+ expect(onResizeEnd).toHaveBeenCalledTimes(3);
+ });
+
+ test('onResizeStart and onResizeEnd are called for left/right keyboard events', () => {
+ const { button, onResizeStart, onResizeEnd } = mountWithCallbacks();
+ button.simulate('keydown', { key: keys.ARROW_RIGHT });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_RIGHT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ button.simulate('keydown', { key: keys.ARROW_LEFT });
+ expect(onResizeStart).toHaveBeenCalledTimes(2);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_LEFT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(2);
+ });
+
+ test('onResizeStart and onResizeEnd are called for up/down keyboard events', () => {
+ const { button, onResizeStart, onResizeEnd } = mountWithCallbacks({
+ direction: 'vertical',
+ });
+ button.simulate('keydown', { key: keys.ARROW_UP });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_UP });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ button.simulate('keydown', { key: keys.ARROW_DOWN });
+ expect(onResizeStart).toHaveBeenCalledTimes(2);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_DOWN });
+ expect(onResizeEnd).toHaveBeenCalledTimes(2);
+ });
+
+ test('onResizeStart and onResizeEnd are called only for the correct keyboard events', () => {
+ const { button, onResizeStart, onResizeEnd } = mountWithCallbacks();
+ button.simulate('keydown', { key: keys.ARROW_DOWN });
+ expect(onResizeStart).toHaveBeenCalledTimes(0);
+ button.simulate('keydown', { key: keys.ARROW_RIGHT });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_DOWN });
+ expect(onResizeEnd).toHaveBeenCalledTimes(0);
+ button.simulate('keyup', { key: keys.ARROW_RIGHT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ });
+
+ test('onResizeStart and onResizeEnd are called correctly when switching resize direction with the keyboard', () => {
+ const { button, onResizeStart, onResizeEnd } = mountWithCallbacks();
+ button.simulate('keydown', { key: keys.ARROW_RIGHT });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keydown', { key: keys.ARROW_LEFT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenCalledTimes(2);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('keyup', { key: keys.ARROW_RIGHT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ button.simulate('keyup', { key: keys.ARROW_LEFT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(2);
+ });
+
+ test('onResizeEnd is called before starting a new resize if a keyboard resize is triggered while a pointer resize is in progress', () => {
+ const {
+ container,
+ button,
+ onResizeStart,
+ onResizeEnd,
+ } = mountWithCallbacks();
+ button.simulate('mousedown', {
+ pageX: 0,
+ pageY: 0,
+ clientX: 0,
+ clientY: 0,
+ });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('pointer');
+ button.simulate('keydown', { key: keys.ARROW_RIGHT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenCalledTimes(2);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ container.simulate('mouseup');
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ button.simulate('keyup', { key: keys.ARROW_RIGHT });
+ expect(onResizeEnd).toHaveBeenCalledTimes(2);
+ });
+
+ test('onResizeEnd is called for keyboard resizes when the button is blurred', () => {
+ const { button, onResizeStart, onResizeEnd } = mountWithCallbacks();
+ button.simulate('keydown', { key: keys.ARROW_RIGHT });
+ expect(onResizeStart).toHaveBeenCalledTimes(1);
+ expect(onResizeStart).toHaveBeenLastCalledWith('key');
+ button.simulate('blur');
+ expect(onResizeEnd).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/src/components/resizable_container/resizable_container.tsx b/src/components/resizable_container/resizable_container.tsx
index 28b1a399351..5ed7e66381e 100644
--- a/src/components/resizable_container/resizable_container.tsx
+++ b/src/components/resizable_container/resizable_container.tsx
@@ -36,9 +36,11 @@ import {
import { useContainerCallbacks, getPosition } from './helpers';
import {
EuiResizableButtonMouseEvent,
- EuiResizableButtonKeyDownEvent,
+ EuiResizableButtonKeyEvent,
EuiResizableContainerState,
EuiResizableContainerActions,
+ KeyMoveDirection,
+ ResizeTrigger,
} from './types';
const containerDirections = {
@@ -66,8 +68,16 @@ export interface EuiResizableContainerProps
* Pure function which accepts an object where keys are IDs of panels, which sizes were changed,
* and values are actual sizes in percents
*/
- onPanelWidthChange?: ({}: { [key: string]: number }) => any;
+ onPanelWidthChange?: ({}: { [key: string]: number }) => void;
onToggleCollapsed?: ToggleCollapseCallback;
+ /**
+ * Called when resizing starts
+ */
+ onResizeStart?: (trigger: ResizeTrigger) => void;
+ /**
+ * Called when resizing ends
+ */
+ onResizeEnd?: () => void;
style?: CSSProperties;
}
@@ -87,6 +97,8 @@ export const EuiResizableContainer: FunctionComponent {
const containerRef = useRef(null);
@@ -122,6 +134,31 @@ export const EuiResizableContainer: FunctionComponent({});
+
+ const resizeEnd = useCallback(() => {
+ onResizeEnd?.();
+ resizeContext.current = {};
+ }, [onResizeEnd]);
+
+ 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();
+ }
+ onResizeStart?.(trigger);
+ resizeContext.current = { trigger, keyMoveDirection };
+ },
+ [onResizeStart, resizeEnd]
+ );
+
const onMouseDown = useCallback(
(event: EuiResizableButtonMouseEvent) => {
const currentTarget = event.currentTarget;
@@ -131,9 +168,10 @@ export const EuiResizableContainer: FunctionComponent {
+ let direction: KeyMoveDirection | null = null;
+ if (
+ (isHorizontal && key === keys.ARROW_LEFT) ||
+ (!isHorizontal && key === keys.ARROW_UP)
+ ) {
+ direction = 'backward';
+ } else if (
+ (isHorizontal && key === keys.ARROW_RIGHT) ||
+ (!isHorizontal && key === keys.ARROW_DOWN)
+ ) {
+ direction = 'forward';
+ }
+ return direction;
+ },
+ [isHorizontal]
+ );
+
const onKeyDown = useCallback(
- (event: EuiResizableButtonKeyDownEvent) => {
+ (event: EuiResizableButtonKeyEvent) => {
const { key, currentTarget } = event;
- const shouldResizeHorizontalPanel =
- isHorizontal && (key === keys.ARROW_LEFT || key === keys.ARROW_RIGHT);
- const shouldResizeVerticalPanel =
- !isHorizontal && (key === keys.ARROW_UP || key === keys.ARROW_DOWN);
+ const direction = getKeyMoveDirection(key);
const prevPanelId = currentTarget.previousElementSibling!.id;
const nextPanelId = currentTarget.nextElementSibling!.id;
- let direction;
- if (key === keys.ARROW_DOWN || key === keys.ARROW_RIGHT) {
- direction = 'forward';
- }
- if (key === keys.ARROW_UP || key === keys.ARROW_LEFT) {
- direction = 'backward';
+
+ if (direction && prevPanelId && nextPanelId) {
+ if (!event.repeat) {
+ resizeStart('key', direction);
+ }
+ event.preventDefault();
+ actions.keyMove({ direction, prevPanelId, nextPanelId });
}
+ },
+ [actions, getKeyMoveDirection, resizeStart]
+ );
+ 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 (
- direction === 'forward' ||
- (direction === 'backward' &&
- (shouldResizeHorizontalPanel || shouldResizeVerticalPanel) &&
- prevPanelId &&
- nextPanelId)
+ resizeContext.current.trigger === 'key' &&
+ resizeContext.current.keyMoveDirection === getKeyMoveDirection(key)
) {
- event.preventDefault();
- actions.keyMove({ direction, prevPanelId, nextPanelId });
+ resizeEnd();
}
},
- [actions, isHorizontal]
+ [getKeyMoveDirection, resizeEnd]
);
const onMouseUp = useCallback(() => {
+ if (resizeContext.current.trigger === 'pointer') {
+ resizeEnd();
+ }
actions.reset();
- }, [actions]);
+ }, [actions, resizeEnd]);
+
+ const onBlur = useCallback(() => {
+ if (resizeContext.current.trigger === 'key') {
+ resizeEnd();
+ }
+ actions.resizerBlur();
+ }, [actions, resizeEnd]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const EuiResizableButton = useCallback(
euiResizableButtonWithControls({
onKeyDown,
+ onKeyUp,
onMouseDown,
onTouchStart: onMouseDown,
onFocus: actions.resizerFocus,
- onBlur: actions.resizerBlur,
+ onBlur,
isHorizontal,
registration: {
register: actions.registerResizer,
diff --git a/src/components/resizable_container/types.ts b/src/components/resizable_container/types.ts
index bcfd3932d15..a02fd128816 100644
--- a/src/components/resizable_container/types.ts
+++ b/src/components/resizable_container/types.ts
@@ -14,6 +14,10 @@ export type PanelPosition = 'first' | 'middle' | 'last';
export type PanelDirection = 'left' | 'right';
+export type KeyMoveDirection = 'forward' | 'backward';
+
+export type ResizeTrigger = 'pointer' | 'key';
+
export interface EuiResizablePanelController {
id: string;
size: number;
@@ -41,7 +45,7 @@ export type EuiResizableButtonMouseEvent =
| MouseEvent
| TouchEvent;
-export type EuiResizableButtonKeyDownEvent = KeyboardEvent;
+export type EuiResizableButtonKeyEvent = KeyboardEvent;
export interface EuiResizableContainerState {
isDragging: boolean;
@@ -82,7 +86,7 @@ export interface ActionKeyMove {
payload: {
prevPanelId: string;
nextPanelId: string;
- direction: 'forward' | 'backward';
+ direction: KeyMoveDirection;
};
}
diff --git a/upcoming_changelogs/6236.md b/upcoming_changelogs/6236.md
new file mode 100644
index 00000000000..6f980a65427
--- /dev/null
+++ b/upcoming_changelogs/6236.md
@@ -0,0 +1 @@
+- Added support for `onResizeStart` and `onResizeEnd` callbacks to `EuiResizableContainer`
\ No newline at end of file