[Expandable Flyout] - customize default right, left and preview widths for push mode (#206155)

## Summary

This PR is making some changes to the Expandable Flyout package. Prior
work had added [push
mode](https://github.com/elastic/kibana/pull/182615) to the package,
added [custom way](https://github.com/elastic/kibana/pull/170078) to
handle the width for multiple resolutions, and [added
support](https://github.com/elastic/kibana/pull/192906) for the internal
section to be resiable by users.

This PR improves the default user experience when using the flyout in
push mode. Until now, the default `right`, `left` and `preview` width in
`push` mode and `overlay` mode were identical. This meant that the
flyout rendered in `push` mode was most of the time using the whole
screen, not leaving any room to the rest of the page content (like the
alerts table).

The `push` widths are now calculated in a different way, to leave as
much room as possible while still allowing the flyout `right` and `left`
sections to render their content correctly, at least most of the time.
Users can still resize the whole flyout as well as the internal `right`
and `left` sections. The `push` widths are generally smaller/narrower
than the `overlay` widths.

#### The `overlay` mode default widths have not changed


https://github.com/user-attachments/assets/28b6c41e-b12c-45cf-aa3e-026a7acdb7b3

#### The `push` mode default widths


https://github.com/user-attachments/assets/93706f9e-212b-4cb4-8748-552f2daed585

### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Philippe Oberti 2025-02-06 22:57:07 +01:00 committed by GitHub
parent ebb31d249f
commit e7140ff25f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 411 additions and 161 deletions

View file

@ -5,6 +5,7 @@
This package offers an expandable flyout UI component and a way to manage the data displayed in it. The component leverages the [EuiFlyout](https://github.com/elastic/eui/tree/main/src/components/flyout) from the EUI library.
The flyout is composed of 3 sections:
- a right section (primary section) that opens first
- a left wider section to show more details
- a preview section, that overlays the right section. This preview section can display multiple panels one after the other and displays a `Back` button
@ -13,14 +14,14 @@ The flyout is composed of 3 sections:
## Design decisions
The expandable-flyout is making some strict UI design decisions:
- when in collapsed mode (i.e. when only the right/preview section is open), the flyout's width linearly grows from its minimum value of 380px to its maximum value of 750px
- when in expanded mode (i.e. when the left section is opened), the flyout's width changes depending on the browser's width:
- if the window is smaller than 1600px, the flyout takes the entire browser window (minus 48px of padding on the left)
- for windows bigger than 1600px, the flyout's width is 80% of the entire browser window (with a max width of 1500px for the left section, and 750px for the right section)
The expandable-flyout offers 2 render modes: push and overlay (leveraged from the use of the see [EUI](https://eui.elastic.co/#/layout/flyout#push-versus-overlay)).
The flyout offers 2 sets of default widths: one for overlay mode and one for push mode. Those width are calculated based on the width of the brower window, and define the default values to be used to render the right, left and preview sections, in a way that is aesthetically pleasing. You can find the details of the calculations [here](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_window_width.ts);
> While the expandable-flyout will work on very small screens, having both the right and left sections visible at the same time will not be a good experience to the user. We recommend only showing the right panel, and therefore handling this situation when you build your panels by considering hiding the actions that could open the left panel (like the expand details button in the [FlyoutNavigation](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx)).
The flyout also offers a way to the users to change the width of the different sections. These are saved separately from the default widths mentioned above, and the users can always reset back to the default using the gear menu (see the `Optional properties` section below).
## State persistence
The expandable flyout offers 2 ways of managing its state:
@ -39,7 +40,6 @@ The second way (done by setting the `urlKey` prop to a string value) saves the s
>
> A good solution is for example to have one instance of a flyout at a page level, and then have multiple panels that can be opened in that flyout.
## Package API
The ExpandableFlyout [React component](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security/packages/expandable-flyout/src/index.tsx) renders the UI, leveraging an [EuiFlyout](https://eui.elastic.co/#/layout/flyout).
@ -49,6 +49,7 @@ To retrieve the flyout's layout (left, right and preview panels), you can utiliz
To control (or mutate) flyout's layout, you can utilize [useExpandableFlyoutApi](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_expandable_flyout_api.ts).
**Expandable Flyout API** exposes the following methods:
- **openFlyout**: open the flyout with a set of panels
- **openRightPanel**: open a right panel
- **openLeftPanel**: open a left panel
@ -66,6 +67,7 @@ When calling `openFlyout`, the right panel state is automatically appended in th
## Usage
To use the expandable flyout in your plugin, first you need wrap your code with the [context provider](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/context.tsx) at a high enough level as follows:
```typescript jsx
// state stored in the url
<ExpandableFlyoutProvider urlKey={'myUrlKey'}>
@ -84,22 +86,26 @@ Then use the [React UI component](https://github.com/elastic/kibana/tree/main/x-
```typescript jsx
<ExpandableFlyout registeredPanels={myPanels} />
```
_where `myPanels` is a list of all the panels that can be rendered in the flyout_
## Optional properties
The expandable flyout now offers a way for users to change some of the flyout's UI properties. These are done via a gear icon in the top right corner of the flyout, to the left of the close icon.
The gear icon can be hidden by setting the `hideSettings` property to `true` in the flyout's custom props.
The `typeDisabled` property allows to disable the push/overlay toggle.
```typescript
flyoutCustomProps?: {
hideSettings?: boolean;
typeDisabled?: boolean,
flyoutCustomProps ? : {
hideSettings? : boolean;
pushVsOverlay? : { disabled: boolean; tooltip: string; };
resize? : { disabled: boolean; tooltip: string; };
};
```
At the moment, clicking on the gear icon opens a popover that allows you to toggle the flyout between `overlay` and `push` modes (see [EUI](https://eui.elastic.co/#/layout/flyout#push-versus-overlay)). The default value is `overlay`. The package remembers the selected value in local storage, only for expandable flyout that have a urlKey. The state of memory flyouts is not persisted.
The gear icon can be hidden by setting the `hideSettings` property to `true` in the flyout's custom props. When shown, clicking on the gear icon opens a popover with the other options rendered in it.
The `pushVsOverlay` property allows to disable the push/overlay toggle and when enabled allows users to switch between the 2 modes (see [EUI](https://eui.elastic.co/#/layout/flyout#push-versus-overlay)). The default value is `overlay`. The package remembers the selected value in local storage, only for expandable flyout that have a urlKey. The state of memory flyouts is not persisted.
The `resize` property allow to disable the `Reset size` button and when enabled allows users to reset all the widths to the default (see calculations [here](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/expandable-flyout/src/hooks/use_window_width.ts)).
## Terminology

View file

@ -153,18 +153,21 @@ export const Container: React.FC<ContainerProps> = memo(
const flyoutWidth = useMemo(() => {
if (showCollapsed) {
return flyoutWidths.collapsedWidth || defaultWidths.rightWidth;
return flyoutWidths.collapsedWidth || defaultWidths[type].rightWidth;
}
if (showExpanded) {
return flyoutWidths.expandedWidth || defaultWidths.rightWidth + defaultWidths.leftWidth;
return (
flyoutWidths.expandedWidth ||
defaultWidths[type].rightWidth + defaultWidths[type].leftWidth
);
}
}, [
showCollapsed,
showExpanded,
defaultWidths,
flyoutWidths.collapsedWidth,
flyoutWidths.expandedWidth,
defaultWidths.rightWidth,
defaultWidths.leftWidth,
showCollapsed,
showExpanded,
type,
]);
// callback function called when user changes the flyout's width

View file

@ -10,14 +10,19 @@ import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiSplitPanel,
EuiText,
useEuiTheme,
EuiSplitPanel,
} from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { css } from '@emotion/react';
import { has } from 'lodash';
import { selectDefaultWidths, selectUserSectionWidths, useSelector } from '../store/redux';
import {
selectDefaultWidths,
selectPushVsOverlay,
selectUserSectionWidths,
useSelector,
} from '../store/redux';
import {
PREVIEW_SECTION_BACK_BUTTON_TEST_ID,
PREVIEW_SECTION_CLOSE_BUTTON_TEST_ID,
@ -85,14 +90,17 @@ export const PreviewSection: React.FC<PreviewSectionProps> = memo(
const { rightPercentage } = useSelector(selectUserSectionWidths);
const defaultPercentages = useSelector(selectDefaultWidths);
const type = useSelector(selectPushVsOverlay);
// Calculate the width of the preview section based on the following
// - if only the right section is visible, then we use 100% of the width (minus some padding)
// - if both the right and left sections are visible, we use the width of the right section (minus the same padding)
const width = useMemo(() => {
const percentage = rightPercentage ? rightPercentage : defaultPercentages.rightPercentage;
const percentage = rightPercentage
? rightPercentage
: defaultPercentages[type].rightPercentage;
return showExpanded ? `calc(${percentage}% - 8px)` : `calc(100% - 8px)`;
}, [defaultPercentages.rightPercentage, rightPercentage, showExpanded]);
}, [defaultPercentages, rightPercentage, showExpanded, type]);
const closeButton = (
<EuiFlexItem grow={false}>

View file

@ -11,6 +11,7 @@ import { css } from '@emotion/react';
import { changeUserSectionWidthsAction } from '../store/actions';
import {
selectDefaultWidths,
selectPushVsOverlay,
selectUserSectionWidths,
useDispatch,
useSelector,
@ -52,15 +53,16 @@ export const ResizableContainer: React.FC<ResizableContainerProps> = memo(
const dispatch = useDispatch();
const { leftPercentage, rightPercentage } = useSelector(selectUserSectionWidths);
const type = useSelector(selectPushVsOverlay);
const defaultPercentages = useSelector(selectDefaultWidths);
const initialLeftPercentage = useMemo(
() => leftPercentage || defaultPercentages.leftPercentage,
[defaultPercentages.leftPercentage, leftPercentage]
() => leftPercentage || defaultPercentages[type].leftPercentage,
[defaultPercentages, leftPercentage, type]
);
const initialRightPercentage = useMemo(
() => rightPercentage || defaultPercentages.rightPercentage,
[defaultPercentages.rightPercentage, rightPercentage]
() => rightPercentage || defaultPercentages[type].rightPercentage,
[defaultPercentages, rightPercentage, type]
);
const onWidthChange = useCallback(

View file

@ -6,14 +6,7 @@
*/
import { renderHook } from '@testing-library/react';
import {
FULL_WIDTH_PADDING,
MAX_RESOLUTION_BREAKPOINT,
MIN_RESOLUTION_BREAKPOINT,
RIGHT_SECTION_MAX_WIDTH,
RIGHT_SECTION_MIN_WIDTH,
useWindowWidth,
} from './use_window_width';
import { useWindowWidth } from './use_window_width';
import { useDispatch } from '../store/redux';
import { setDefaultWidthsAction } from '../store/actions';
@ -48,7 +41,7 @@ describe('useWindowWidth', () => {
expect(mockUseDispatch).not.toHaveBeenCalled();
});
it('should handle very small screens', () => {
it('should handle screens below 380px', () => {
global.innerWidth = 300;
const mockUseDispatch = jest.fn();
@ -59,14 +52,17 @@ describe('useWindowWidth', () => {
expect(hookResult.result.current).toEqual(300);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
left: -48,
right: 300,
preview: 300,
leftOverlay: -48,
leftPush: 380,
previewOverlay: 300,
previewPush: 300,
rightOverlay: 300,
rightPush: 300,
})
);
});
it('should handle small screens', () => {
it('should handle screens between 380px and 992px', () => {
global.innerWidth = 500;
const mockUseDispatch = jest.fn();
@ -77,58 +73,99 @@ describe('useWindowWidth', () => {
expect(hookResult.result.current).toEqual(500);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
left: 72,
right: 380,
preview: 380,
leftOverlay: 72,
leftPush: 380,
previewOverlay: 380,
previewPush: 380,
rightOverlay: 380,
rightPush: 380,
})
);
});
it('should handle medium screens', () => {
global.innerWidth = 1300;
it('should handle screens between 992px and 1600px', () => {
global.innerWidth = 1000;
const mockUseDispatch = jest.fn();
(useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch);
const hookResult = renderHook(() => useWindowWidth());
const right =
RIGHT_SECTION_MIN_WIDTH +
(RIGHT_SECTION_MAX_WIDTH - RIGHT_SECTION_MIN_WIDTH) *
((1300 - MIN_RESOLUTION_BREAKPOINT) /
(MAX_RESOLUTION_BREAKPOINT - MIN_RESOLUTION_BREAKPOINT));
const left = 1300 - right - FULL_WIDTH_PADDING;
const preview = right;
const rightOverlay = 380 + (750 - 380) * ((1000 - 992) / (1920 - 992));
const leftOverlay = 1000 - rightOverlay - 48;
const previewOverlay = rightOverlay;
const rightPush = 380 + (600 - 380) * ((1000 - 1600) / (2560 - 1600));
const leftPush = 380;
const previewPush = rightPush;
expect(hookResult.result.current).toEqual(1300);
expect(hookResult.result.current).toEqual(1000);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
left,
right,
preview,
rightOverlay,
leftOverlay,
previewOverlay,
leftPush,
previewPush,
rightPush,
})
);
});
it('should handle large screens', () => {
global.innerWidth = 2500;
it('should handle screens between 1600px and 1920', () => {
global.innerWidth = 1800;
const mockUseDispatch = jest.fn();
(useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch);
const hookResult = renderHook(() => useWindowWidth());
expect(hookResult.result.current).toEqual(2500);
const rightOverlay = 380 + (750 - 380) * ((1800 - 992) / (1920 - 992));
const leftOverlay = ((1800 - rightOverlay) * 80) / 100;
const previewOverlay = rightOverlay;
const rightPush = 380 + (600 - 380) * ((1800 - 1600) / (2560 - 1600));
const leftPush = ((1800 - rightPush - 200) * 40) / 100;
const previewPush = rightPush;
expect(hookResult.result.current).toEqual(1800);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
left: 1400,
right: 750,
preview: 750,
rightOverlay,
leftOverlay,
previewOverlay,
leftPush,
previewPush,
rightPush,
})
);
});
it('should handle very large screens', () => {
it('should handle screens between 1920px and 2560px', () => {
global.innerWidth = 2400;
const mockUseDispatch = jest.fn();
(useDispatch as jest.Mock).mockImplementation(() => mockUseDispatch);
const hookResult = renderHook(() => useWindowWidth());
const leftOverlay = ((2400 - 750) * 80) / 100;
const rightPush = 380 + (600 - 380) * ((2400 - 1600) / (2560 - 1600));
const leftPush = ((2400 - rightPush - 200) * 40) / 100;
const previewPush = rightPush;
expect(hookResult.result.current).toEqual(2400);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
rightOverlay: 750,
leftOverlay,
previewOverlay: 750,
leftPush,
previewPush,
rightPush,
})
);
});
it('should handle screens above 2560px', () => {
global.innerWidth = 3800;
const mockUseDispatch = jest.fn();
@ -139,9 +176,12 @@ describe('useWindowWidth', () => {
expect(hookResult.result.current).toEqual(3800);
expect(mockUseDispatch).toHaveBeenCalledWith(
setDefaultWidthsAction({
left: 1500,
right: 750,
preview: 750,
leftOverlay: 1500,
leftPush: 1200,
previewOverlay: 750,
previewPush: 600,
rightOverlay: 750,
rightPush: 600,
})
);
});

View file

@ -9,18 +9,143 @@ import { useLayoutEffect, useState } from 'react';
import { useDispatch } from '../store/redux';
import { setDefaultWidthsAction } from '../store/actions';
export const RIGHT_SECTION_MIN_WIDTH = 380;
export const MIN_RESOLUTION_BREAKPOINT = 992;
export const RIGHT_SECTION_MAX_WIDTH = 750;
export const MAX_RESOLUTION_BREAKPOINT = 1920;
const RESOLUTION_BREAKPOINTS = {
RIGHT: {
MIN: 992, // resolution below which the width is fixed to 380px
OVERLAY_MAX: 1920, // resolution above which the overlay width is fixed to 750px
PUSH_MIN: 1600, // resolution below which the push width is fixed to 380px
PUSH_MAX: 2560, // resolution above which the push width is fixed to 600px
},
LEFT: {
MIN: 1600, // resolution below which the overlay width goes full width (minus the padding) and the push width goes to its fixed 380px
},
};
const FULL_WIDTH_PADDING = 48;
const NAVIGATION_WIDTH = 200;
const LEFT_SECTION_MAX_WIDTH = 1500;
const FULL_WIDTH_BREAKPOINT = 1600;
export const FULL_WIDTH_PADDING = 48;
const SECTION_WIDTHS = {
RIGHT: {
MIN: 380,
OVERLAY_MAX: 750,
PUSH_MAX: 600,
},
LEFT: {
OVERLAY_MAX: 1500,
PUSH_MIN: 380,
},
};
/**
* Hook that returns the browser window width
* Calculates the default widths for the right section of the expandable flyout in push and overlay modes.
*
* For overlay mode, the flyout right section scales as follows:
* - for window widths below 380px, we make sure that the width is identical to the window width
* - for window widths between 380px and 992px, the width is fixed at 380px
* - for window widths between 992px and 1920px, the width scales linearly between 380px and 750px
* - for window widths above 1920px, the width is fixed at 750px
*
* For push mode, the flyout right section scales as follows:
* - for window widths below 380px, we make sure that the flyout width is identical to the window width (also, EUI actually automatically switches the flyout to overlay mode)
* - for window widths between 380px and 1600px, the width is fixed at 380px
* - for window widths between 1600x and 2560px, the width scales linearly between 380px and 600px
* - for window widths above 2560px, the width is fixed at 600px
*/
const calculateRightSectionDefaultWidths = (
windowWidth: number
): {
overlay: number;
push: number;
} => {
// for tiny window widths (less than 380px), we want to make sure the flyout will not go outside the window width
if (windowWidth < SECTION_WIDTHS.RIGHT.MIN) {
return {
overlay: windowWidth,
push: windowWidth,
};
}
// for window widths between 380px and 992px, the width is fixed to the 380px
// EUI automatically switches a push flyout to overlay below 992px, so the push value here is actually a bit unnecessary but we return it anyway to ensure the redux store is always populated with a value
if (windowWidth < RESOLUTION_BREAKPOINTS.RIGHT.MIN) {
return {
overlay: SECTION_WIDTHS.RIGHT.MIN,
push: SECTION_WIDTHS.RIGHT.MIN,
};
}
// in overlay mode, the width will linearly scale from 380px (at 992px resolution) to 750px (at 1920px resolution)
const ratioWidthOverlayMode =
(SECTION_WIDTHS.RIGHT.OVERLAY_MAX - SECTION_WIDTHS.RIGHT.MIN) *
((windowWidth - RESOLUTION_BREAKPOINTS.RIGHT.MIN) /
(RESOLUTION_BREAKPOINTS.RIGHT.OVERLAY_MAX - RESOLUTION_BREAKPOINTS.RIGHT.MIN));
// this will ensure that in push in mode the width will never go bigger than 750px in higher resolutions
const overlayWidth = Math.min(
SECTION_WIDTHS.RIGHT.MIN + ratioWidthOverlayMode,
SECTION_WIDTHS.RIGHT.OVERLAY_MAX
);
// in push mode, the width will linearly scale from 380px (at 1600px resolution) to 600px (at 2560px resolution)
const ratioWidthPushMode =
(SECTION_WIDTHS.RIGHT.PUSH_MAX - SECTION_WIDTHS.RIGHT.MIN) *
((windowWidth - RESOLUTION_BREAKPOINTS.RIGHT.PUSH_MIN) /
(RESOLUTION_BREAKPOINTS.RIGHT.PUSH_MAX - RESOLUTION_BREAKPOINTS.RIGHT.PUSH_MIN));
// this will ensure that in push mode the width will never go bigger than 600px in higher resolutions
const pushWidth = Math.min(
SECTION_WIDTHS.RIGHT.MIN + ratioWidthPushMode,
SECTION_WIDTHS.RIGHT.PUSH_MAX
);
return {
overlay: overlayWidth,
push: pushWidth,
};
};
/**
* Calculates the default widths for the left section of the expandable flyout in push and overlay modes.
*
* For overlay mode, the flyout left section scales as follows:
* - for window widths below 1600px, the width is taking the full screen minus a 48px padding
* - for window widths above 1600px, the width corresponds to 80% of the remaining space
*
* For push mode, the flyout left section scales as follows:
* - for window widths below 1600px, the width is fixed at 380px
* - for window widths above 1600px, the width corresponds to 40% of the remaining space
*/
const calculateLeftSectionDefaultWidths = (
windowWidth: number,
rightSectionWidthOverlay: number,
rightSectionWidthPush: number
): {
overlay: number;
push: number;
} => {
// for window widths below 1600px, the overlay width will use the remaining space (minus a small padding)
// for window widths above 1600px, the overlay width will use 80% of the remaining space, while never going bigger than 1500px
const overlayWidth =
windowWidth <= RESOLUTION_BREAKPOINTS.LEFT.MIN
? windowWidth - rightSectionWidthOverlay - FULL_WIDTH_PADDING
: Math.min(
((windowWidth - rightSectionWidthOverlay) * 80) / 100,
SECTION_WIDTHS.LEFT.OVERLAY_MAX
);
// for window widths below 1600px, the push width will be fixed to 380px
// for window widths above 1600px, the push width will use 40% of the remaining space (excluding the navigation width)
const pushWidth =
windowWidth <= RESOLUTION_BREAKPOINTS.LEFT.MIN
? SECTION_WIDTHS.LEFT.PUSH_MIN
: ((windowWidth - rightSectionWidthPush - NAVIGATION_WIDTH) * 40) / 100;
return {
overlay: overlayWidth,
push: pushWidth,
};
};
/**
* Hook that returns the browser window width.
* It also calculates all the default widths values for the flyout to render in overlay and push modes then stores them in Redux.
*/
export const useWindowWidth = (): number => {
const dispatch = useDispatch();
@ -32,47 +157,35 @@ export const useWindowWidth = (): number => {
setWidth(window.innerWidth);
const windowWidth = window.innerWidth;
// if the browser's window width is 0 (which should only happen the very first time this hook is called) there is no point in calculating all the default flyout's widths
if (windowWidth !== 0) {
let rightSectionWidth: number;
if (windowWidth < MIN_RESOLUTION_BREAKPOINT) {
// the right section's width will grow from 380px (at 992px resolution) while handling tiny screens by not going smaller than the window width
rightSectionWidth = Math.min(RIGHT_SECTION_MIN_WIDTH, windowWidth);
} else {
const ratioWidth =
(RIGHT_SECTION_MAX_WIDTH - RIGHT_SECTION_MIN_WIDTH) *
((windowWidth - MIN_RESOLUTION_BREAKPOINT) /
(MAX_RESOLUTION_BREAKPOINT - MIN_RESOLUTION_BREAKPOINT));
const { overlay: rightSectionWidthOverlay, push: rightSectionWidthPush } =
calculateRightSectionDefaultWidths(windowWidth);
// the right section's width will grow to 750px (at 1920px resolution) and will never go bigger than 750px in higher resolutions
rightSectionWidth = Math.min(
RIGHT_SECTION_MIN_WIDTH + ratioWidth,
RIGHT_SECTION_MAX_WIDTH
const { overlay: leftSectionWidthOverlay, push: leftSectionWidthPush } =
calculateLeftSectionDefaultWidths(
windowWidth,
rightSectionWidthOverlay,
rightSectionWidthPush
);
}
let leftSectionWidth: number;
// the left section's width will be nearly the remaining space for resolution lower than 1600px
if (windowWidth <= FULL_WIDTH_BREAKPOINT) {
leftSectionWidth = windowWidth - rightSectionWidth - FULL_WIDTH_PADDING;
} else {
// the left section's width will be taking 80% of the remaining space for resolution higher than 1600px, while never going bigger than 1500px
leftSectionWidth = Math.min(
((windowWidth - rightSectionWidth) * 80) / 100,
LEFT_SECTION_MAX_WIDTH
);
}
const previewSectionWidth: number = rightSectionWidth;
const previewSectionWidthOverlay: number = rightSectionWidthOverlay;
const previewSectionWidthPush: number = rightSectionWidthPush;
dispatch(
setDefaultWidthsAction({
right: rightSectionWidth,
left: leftSectionWidth,
preview: previewSectionWidth,
rightOverlay: rightSectionWidthOverlay,
leftOverlay: leftSectionWidthOverlay,
previewOverlay: previewSectionWidthOverlay,
rightPush: rightSectionWidthPush,
leftPush: leftSectionWidthPush,
previewPush: previewSectionWidthPush,
})
);
}
}
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);

View file

@ -144,17 +144,29 @@ export const changePushVsOverlayAction = createAction<{
export const setDefaultWidthsAction = createAction<{
/**
* Default width for the right section
* Default width for the right section in overlay mode
*/
right: number;
rightOverlay: number;
/**
* Default width for the left section
* Default width for the left section in overlay mode
*/
left: number;
leftOverlay: number;
/**
* Default width for the preview section
* Default width for the preview section in overlay mode
*/
preview: number;
previewOverlay: number;
/**
* Default width for the right section in push mode
*/
rightPush: number;
/**
* Default width for the left section in push mode
*/
leftPush: number;
/**
* Default width for the preview section in push mode
*/
previewPush: number;
}>(ActionType.setDefaultWidths);
export const changeUserCollapsedWidthAction = createAction<{

View file

@ -7,7 +7,7 @@
import { FlyoutPanelProps } from '../types';
import { panelsReducer, uiReducer } from './reducers';
import { initialPanelsState, PanelsState, initialUiState, UiState } from './state';
import { initialPanelsState, initialUiState, PanelsState, UiState } from './state';
import {
changePushVsOverlayAction,
changeUserCollapsedWidthAction,
@ -874,21 +874,34 @@ describe('uiReducer', () => {
it('should set value state is empty', () => {
const state: UiState = initialUiState;
const action = setDefaultWidthsAction({
right: 200,
left: 600,
preview: 200,
rightOverlay: 300,
leftOverlay: 900,
previewOverlay: 300,
rightPush: 200,
leftPush: 600,
previewPush: 200,
});
const newState: UiState = uiReducer(state, action);
expect(newState).toEqual({
...state,
defaultWidths: {
rightWidth: 200,
leftWidth: 600,
previewWidth: 200,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
overlay: {
rightWidth: 300,
leftWidth: 900,
previewWidth: 300,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
},
push: {
rightWidth: 200,
leftWidth: 600,
previewWidth: 200,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
},
},
});
});
@ -897,30 +910,53 @@ describe('uiReducer', () => {
const state: UiState = {
...initialUiState,
defaultWidths: {
rightWidth: 200,
leftWidth: 600,
previewWidth: 200,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
overlay: {
rightWidth: 300,
leftWidth: 900,
previewWidth: 300,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
},
push: {
rightWidth: 200,
leftWidth: 600,
previewWidth: 200,
rightPercentage: 25,
leftPercentage: 75,
previewPercentage: 25,
},
},
};
const action = setDefaultWidthsAction({
right: 500,
left: 500,
preview: 500,
rightOverlay: 500,
leftOverlay: 500,
previewOverlay: 500,
rightPush: 500,
leftPush: 500,
previewPush: 500,
});
const newState: UiState = uiReducer(state, action);
expect(newState).toEqual({
...state,
defaultWidths: {
rightWidth: 500,
leftWidth: 500,
previewWidth: 500,
rightPercentage: 50,
leftPercentage: 50,
previewPercentage: 50,
overlay: {
rightWidth: 500,
leftWidth: 500,
previewWidth: 500,
rightPercentage: 50,
leftPercentage: 50,
previewPercentage: 50,
},
push: {
rightWidth: 500,
leftWidth: 500,
previewWidth: 500,
rightPercentage: 50,
leftPercentage: 50,
previewPercentage: 50,
},
},
});
});

View file

@ -8,22 +8,22 @@
import { createReducer } from '@reduxjs/toolkit';
import deepEqual from 'react-fast-compare';
import {
openPanelsAction,
openLeftPanelAction,
openRightPanelAction,
closePanelsAction,
closeLeftPanelAction,
closePreviewPanelAction,
closeRightPanelAction,
previousPreviewPanelAction,
openPreviewPanelAction,
urlChangedAction,
changePushVsOverlayAction,
setDefaultWidthsAction,
changeUserCollapsedWidthAction,
changeUserExpandedWidthAction,
changeUserSectionWidthsAction,
closeLeftPanelAction,
closePanelsAction,
closePreviewPanelAction,
closeRightPanelAction,
openLeftPanelAction,
openPanelsAction,
openPreviewPanelAction,
openRightPanelAction,
previousPreviewPanelAction,
resetAllUserChangedWidthsAction,
setDefaultWidthsAction,
urlChangedAction,
} from './actions';
import { initialPanelsState, initialUiState } from './state';
@ -167,14 +167,30 @@ export const uiReducer = createReducer(initialUiState, (builder) => {
state.pushVsOverlay = type;
});
builder.addCase(setDefaultWidthsAction, (state, { payload: { right, left, preview } }) => {
state.defaultWidths.rightWidth = right;
state.defaultWidths.leftWidth = left;
state.defaultWidths.previewWidth = preview;
state.defaultWidths.rightPercentage = (right / (right + left)) * 100;
state.defaultWidths.leftPercentage = (left / (right + left)) * 100;
state.defaultWidths.previewPercentage = (right / (right + left)) * 100;
});
builder.addCase(
setDefaultWidthsAction,
(
state,
{ payload: { rightOverlay, leftOverlay, previewOverlay, rightPush, leftPush, previewPush } }
) => {
state.defaultWidths.overlay.rightWidth = rightOverlay;
state.defaultWidths.overlay.leftWidth = leftOverlay;
state.defaultWidths.overlay.previewWidth = previewOverlay;
state.defaultWidths.overlay.rightPercentage =
(rightOverlay / (rightOverlay + leftOverlay)) * 100;
state.defaultWidths.overlay.leftPercentage =
(leftOverlay / (rightOverlay + leftOverlay)) * 100;
state.defaultWidths.overlay.previewPercentage =
(previewOverlay / (previewOverlay + leftOverlay)) * 100;
state.defaultWidths.push.rightWidth = rightPush;
state.defaultWidths.push.leftWidth = leftPush;
state.defaultWidths.push.previewWidth = previewPush;
state.defaultWidths.push.rightPercentage = (rightPush / (rightPush + leftPush)) * 100;
state.defaultWidths.push.leftPercentage = (leftPush / (rightPush + leftPush)) * 100;
state.defaultWidths.push.previewPercentage = (previewPush / (previewPush + leftPush)) * 100;
}
);
builder.addCase(changeUserCollapsedWidthAction, (state, { payload: { width } }) => {
state.userFlyoutWidths.collapsedWidth = width;

View file

@ -12,10 +12,10 @@ import { createSelector } from 'reselect';
import { panelsReducer, uiReducer } from './reducers';
import { initialState, State } from './state';
import {
savePushVsOverlayToLocalStorageMiddleware,
saveUserSectionWidthsToLocalStorageMiddleware,
saveUserFlyoutWidthsToLocalStorageMiddleware,
clearAllUserWidthsFromLocalStorageMiddleware,
savePushVsOverlayToLocalStorageMiddleware,
saveUserFlyoutWidthsToLocalStorageMiddleware,
saveUserSectionWidthsToLocalStorageMiddleware,
} from './middlewares';
export const store = configureStore({

View file

@ -46,7 +46,7 @@ export const initialPanelsState: PanelsState = {
needsSync: false,
};
export interface DefaultWidthsState {
interface DefaultSectionWidthsState {
/**
* Default width for the right section (calculated from the window width)
*/
@ -73,6 +73,17 @@ export interface DefaultWidthsState {
previewPercentage: number;
}
export interface DefaultWidthsState {
/**
* Default width for all the sections in overlay mode
*/
overlay: DefaultSectionWidthsState;
/**
* Default widths for all the sections in push mode
*/
push: DefaultSectionWidthsState;
}
export interface UserFlyoutWidthsState {
/**
* Width of the collapsed flyout
@ -116,7 +127,10 @@ export interface UiState {
export const initialUiState: UiState = {
pushVsOverlay: 'overlay',
defaultWidths: {} as DefaultWidthsState,
defaultWidths: {
overlay: {},
push: {},
} as DefaultWidthsState,
userFlyoutWidths: {},
userSectionWidths: {} as UserSectionWidthsState,
};