From 03cdc7b993eacb76eab7fef3e7cb0be21b0bceec Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 10 Dec 2020 09:26:52 -0700 Subject: [PATCH 1/2] Fix docs site to not intercept hash-only links (#4371) --- src-docs/src/views/link_wrapper.js | 7 ++----- src/components/table/table_pagination/table_pagination.tsx | 2 -- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src-docs/src/views/link_wrapper.js b/src-docs/src/views/link_wrapper.js index faaf0a27f21d..f1aa96a5dcd0 100644 --- a/src-docs/src/views/link_wrapper.js +++ b/src-docs/src/views/link_wrapper.js @@ -21,11 +21,8 @@ export const LinkWrapper = ({ children }) => { if (anchor && anchor.nodeName === 'A') { const href = anchor.getAttribute('href'); // check if this is an internal link - if (href.startsWith('#')) { - if (href !== '#') { - // a lone # character is used in the docs as a placeholder/demo value and should be ignored - history.push(href.replace('#', '')); - } + if (href.startsWith('#/')) { + history.push(href.replace('#', '')); e.preventDefault(); } } diff --git a/src/components/table/table_pagination/table_pagination.tsx b/src/components/table/table_pagination/table_pagination.tsx index 1f5dddb5533d..5540ccca4ba0 100644 --- a/src/components/table/table_pagination/table_pagination.tsx +++ b/src/components/table/table_pagination/table_pagination.tsx @@ -73,7 +73,6 @@ export class EuiTablePagination extends Component { onChangeItemsPerPage = () => {}, onChangePage, pageCount, - 'aria-controls': ariaControls, ...rest } = this.props; @@ -132,7 +131,6 @@ export class EuiTablePagination extends Component { Date: Thu, 10 Dec 2020 11:43:44 -0500 Subject: [PATCH 2/2] [EuiResizableContainer] Allow collapsible panels (#3978) * will add custom icons * playing with props * adding example files * working on the example * example is dynamic * cleaner example * add class to button * cl * fix dom structure warnings * refactor; reducer pattern * snapshots; clean up * vertical collapsibles; combined registration * WIP * WIP: showOnFocus * POC: mode prop * memoize actions; toggle button placement * clean up * clean up * better vertical collapsibles * external toggle example * better resizer focus state; fallback redistribution logic * add shadow to toggle button * shift button when collapsed; blur on mouse click * clean up; inline side-effects * cleaner examples * snapshots * docs * adjustments for collapsible options * clean up actions * more docs, snippets * remove custom icons; update example * account for more external toggling situations; update rotated icons * larger click area for collapsed toggle button * Using and EuiPanel as the `.euiResizablePanel__content` - Added `paddingSize` to EuiPage * Fixing the buttons - Removed size option from resizable button and changed to `0` width via negative margins - Updated toggle button positioning Still need to update examples and tests * address padding; collapsedContent * collapsedContent -> collapsedButton * new icons * clean up * position prop; remove collapsedButton * Consolidate and organize a few things - Made the EuiCollapsiblePanel and extension of EuiPanel with `wrapperProps` and `wrapperPadding` instead of the other way around - Added `left | right` positions specifically for the vertical layout - Created external EuiResizableCollapseButton for easier maintenance - Continue to show the EuiPanel to keep the visuals when collapsed (if showing collapsed button) - Fixed up examples/docs * 0 size on collapse * allow toggle blur * maintain focus; maintain state while collapsed * Apply suggestions from code review Co-authored-by: Chandler Prall * null check * small review updates * optional externalPosition * toggle button focus * add some tests * remove async * CL * Apply suggestions from code review Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * remove callout from intro Co-authored-by: Greg Thompson Co-authored-by: cchaos Co-authored-by: Chandler Prall Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> --- CHANGELOG.md | 7 + .../resizable_container_basic.js | 8 +- .../resizable_container_example.js | 342 +++++++-- .../resizable_container_reset_values.js | 8 +- .../resizable_container_three_panels.js | 35 - .../resizable_container_vertical.js | 8 +- .../resizable_panel_collapsible.js | 164 ++++ .../resizable_panel_collapsible_external.js | 123 +++ .../resizable_panel_collapsible_options.js | 104 +++ .../resizable_container/resizable_panels.js | 63 ++ .../resizable_resizer_size.js | 69 -- .../page/__snapshots__/page.test.tsx.snap | 8 +- src/components/page/_page.scss | 9 +- src/components/page/page.tsx | 36 +- src/components/panel/index.ts | 2 +- src/components/panel/panel.tsx | 13 +- .../resizable_container.test.tsx.snap | 243 ++++-- .../resizable_container/_index.scss | 1 + .../_resizable_button.scss | 32 +- .../_resizable_collapse_button.scss | 152 ++++ .../resizable_container/_resizable_panel.scss | 47 ++ .../resizable_container/_variables.scss | 10 +- .../resizable_container/context.tsx | 73 +- .../resizable_container/helpers.test.ts | 66 ++ src/components/resizable_container/helpers.ts | 720 +++++++++++++----- .../resizable_container/resizable_button.tsx | 83 +- .../resizable_collapse_button.tsx | 98 +++ .../resizable_container.test.tsx | 61 +- .../resizable_container.tsx | 188 ++++- .../resizable_container/resizable_panel.tsx | 335 +++++++- src/components/resizable_container/types.ts | 198 +++++ 31 files changed, 2681 insertions(+), 625 deletions(-) delete mode 100644 src-docs/src/views/resizable_container/resizable_container_three_panels.js create mode 100644 src-docs/src/views/resizable_container/resizable_panel_collapsible.js create mode 100644 src-docs/src/views/resizable_container/resizable_panel_collapsible_external.js create mode 100644 src-docs/src/views/resizable_container/resizable_panel_collapsible_options.js create mode 100644 src-docs/src/views/resizable_container/resizable_panels.js delete mode 100644 src-docs/src/views/resizable_container/resizable_resizer_size.js create mode 100644 src/components/resizable_container/_resizable_collapse_button.scss create mode 100644 src/components/resizable_container/helpers.test.ts create mode 100644 src/components/resizable_container/resizable_collapse_button.tsx create mode 100644 src/components/resizable_container/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4085c85869ba..26e434cd7057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added collapsble behavior to `EuiResizableContainer` panels ([#3978](https://github.com/elastic/eui/pull/3978)) +- Updated `EuiResizablePanel` to use `EuiPanel` ([#3978](https://github.com/elastic/eui/pull/3978)) + **Bug fixes** - Fixed `EuiSuggest` popover opening when an empty array is passed into the `suggestions` prop ([#4349](https://github.com/elastic/eui/pull/4349)) +**Breaking changes** + +- Removed `size` prop from `EuiResizableButton` ([#3978](https://github.com/elastic/eui/pull/3978)) + ## [`30.6.0`](https://github.com/elastic/eui/tree/v30.6.0) - Adjusted the shadow in `EuiComment` ([#4321](https://github.com/elastic/eui/pull/4321)) diff --git a/src-docs/src/views/resizable_container/resizable_container_basic.js b/src-docs/src/views/resizable_container/resizable_container_basic.js index 3c4f9eeffffa..c9e1d4fc1591 100644 --- a/src-docs/src/views/resizable_container/resizable_container_basic.js +++ b/src-docs/src/views/resizable_container/resizable_container_basic.js @@ -11,12 +11,12 @@ const text = ( ); export default () => ( - + {(EuiResizablePanel, EuiResizableButton) => ( <> -

{text}

+
{text}
Hello world
@@ -24,9 +24,7 @@ export default () => ( - -

{text}

-
+ {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 759edda7017a..a2d817755e6d 100644 --- a/src-docs/src/views/resizable_container/resizable_container_example.js +++ b/src-docs/src/views/resizable_container/resizable_container_example.js @@ -1,4 +1,5 @@ -import React, { Fragment } from 'react'; +import React from 'react'; +import { Link } from 'react-router-dom'; import { renderToHtml } from '../../services'; @@ -7,40 +8,117 @@ import { GuideSectionTypes } from '../../components'; import { EuiCallOut, EuiCode, + EuiCodeBlock, EuiLink, EuiResizableContainer, EuiSpacer, EuiText, } from '../../../../src/components'; +// eslint-disable-next-line import { EuiResizablePanel } from '../../../../src/components/resizable_container/resizable_panel'; import { EuiResizableButton } from '../../../../src/components/resizable_container/resizable_button'; +// eslint-disable-next-line +import { ModeOptions, ToggleOptions } from '!!prop-loader!../../../../src/components/resizable_container/resizable_panel'; +import { PanelModeType } from '!!prop-loader!../../../../src/components/resizable_container/types'; + import ResizableContainerBasic from './resizable_container_basic'; import ResizableContainerVertical from './resizable_container_vertical'; -import ResizableContainerThreePanels from './resizable_container_three_panels'; import ResizableContainerResetValues from './resizable_container_reset_values'; -import ResizableResizerSize from './resizable_resizer_size'; +import ResizablePanels from './resizable_panels'; +import ResizablePanelCollapsible from './resizable_panel_collapsible'; +import ResizablePanelCollapsibleOpts from './resizable_panel_collapsible_options'; +import ResizablePanelCollapsibleExt from './resizable_panel_collapsible_external'; const ResizableContainerSource = require('!!raw-loader!./resizable_container_basic'); -const ResizableContainerVericalSource = require('!!raw-loader!./resizable_container_vertical'); -const ResizableContainerThreePanelsSource = require('!!raw-loader!./resizable_container_three_panels'); +const ResizableContainerVerticalSource = require('!!raw-loader!./resizable_container_vertical'); const ResizableContainerResetValuesSource = require('!!raw-loader!./resizable_container_reset_values'); -const ResizableResizerSizeSource = require('!!raw-loader!./resizable_resizer_size'); +const ResizablePanelsSource = require('!!raw-loader!./resizable_panels'); +const ResizablePanelCollapsibleSource = require('!!raw-loader!./resizable_panel_collapsible'); +const ResizablePanelCollapsibleOptsSource = require('!!raw-loader!./resizable_panel_collapsible_options'); +const ResizablePanelCollapsibleExtSource = require('!!raw-loader!./resizable_panel_collapsible_external'); const ResizableContainerHtml = renderToHtml(ResizableContainerBasic); -const ResizableContainerVericalHtml = renderToHtml(ResizableContainerVertical); -const ResizableContainerThreePanelsHtml = renderToHtml( - ResizableContainerThreePanels -); +const basicSnippet = ` + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + + )} +`; + +const ResizablePanelsHtml = renderToHtml(ResizablePanels); +const panelsSnippet = ` + +

{text}

+
+
`; + +const ResizableContainerVerticalHtml = renderToHtml(ResizableContainerVertical); +const verticalSnippet = ` + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + +

{text}

+
+
+ + + + + +

{text}

+
+
+ + )} +
`; const ResizableContainerResetValuesHtml = renderToHtml( ResizableContainerResetValues ); -const ResizableResizerSizeHtml = renderToHtml(ResizableResizerSize); +const ResizablePanelCollapsibleHtml = renderToHtml(ResizablePanelCollapsible); +const collapsibleSnippet = ` + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + +

{text}

+
+
+ + -const snippet = ` + + +

{text}

+
+
+ + )} +
`; +const ResizablePanelCollapsibleOptsHtml = renderToHtml( + ResizablePanelCollapsibleOpts +); +const collapsibleOptsSnippet = ` {(EuiResizablePanel, EuiResizableButton) => ( <> - +

{text}

@@ -48,7 +126,29 @@ const snippet = ` - + + +

{text}

+
+
+ + )} +
`; +const ResizablePanelCollapsibleExtHtml = renderToHtml( + ResizablePanelCollapsibleExt +); +const collapsibleExtSnippet = ` + {(EuiResizablePanel, EuiResizableButton, {togglePanel}) => ( + <> + + +

{text}

+
+
+ + + +

{text}

@@ -61,28 +161,25 @@ export const ResizableContainerExample = { title: 'Resizable container', isNew: true, intro: ( - - - -

- This component is handy for various resizable containers.{' '} - EuiResizableContainer uses the{' '} - - React Render Props - {' '} - technique to provide EuiResizablePanel and{' '} - EuiResizableButton components for you layout. Wrap - parts of your content with the EuiResizablePanel{' '} - component and put the EuiResizableButton component - between. -

-
-
+ +

+ This component is handy for various resizable containers.{' '} + EuiResizableContainer uses the{' '} + + React Render Props + {' '} + technique to provide EuiResizablePanel and{' '} + EuiResizableButton components for layout, and{' '} + actions for custom handling collapse and resize + functionality in your app. Wrap parts of your content with the{' '} + EuiResizablePanel component and put the{' '} + EuiResizableButton component between. +

-
+ ), sections: [ { @@ -111,7 +208,7 @@ export const ResizableContainerExample = {
  • add initialSize in percents to each panel to specify the initial size of it. Other calculations will be - incapsulated, you don't worry about it. + encapsulated, you don't worry about it.
  • add scrollable=false prop to a panel to @@ -120,10 +217,49 @@ export const ResizableContainerExample = { ), - props: { EuiResizableContainer, EuiResizablePanel, EuiResizableButton }, - snippet, + props: { + EuiResizableContainer, + EuiResizablePanel, + EuiResizableButton, + }, + snippet: basicSnippet, demo: , }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: ResizablePanelsSource, + }, + { + type: GuideSectionTypes.HTML, + code: ResizablePanelsHtml, + }, + ], + title: 'Resizable panel options', + text: ( +
    +

    + Each EuiResizablePanel is simply an{' '} + EuiPanel wrapped with a{' '} + {'

    '} for controlling the width. It stretches + to fill its container and accepts all of the same{' '} + + EuiPanel + {' '} + props to style your panel. +

    +

    + The default props clear most of the EuiPanel{' '} + styles, but you can add them back in with color,{' '} + hasShadow, and paddingSize. +

    +
    + ), + props: { EuiResizablePanel }, + snippet: panelsSnippet, + demo: , + }, { source: [ { @@ -166,77 +302,147 @@ export const ResizableContainerExample = { source: [ { type: GuideSectionTypes.JS, - code: ResizableContainerThreePanelsSource, + code: ResizableContainerVerticalSource, }, { type: GuideSectionTypes.HTML, - code: ResizableContainerThreePanelsHtml, + code: ResizableContainerVerticalHtml, }, ], - title: 'Horizontal resizing with three panels', + title: 'Vertical resizing', text: (

    - The EuiResizablePanel and{' '} - EuiResizableButton components can each be used - multiple times to create a more complex layout. + Set direction=vertical on{' '} + EuiResizableContainer to set a vertical orientation + of the resizable panels.

    ), props: { EuiResizableContainer, EuiResizablePanel, EuiResizableButton }, - demo: , + demo: , + snippet: verticalSnippet, }, { source: [ { type: GuideSectionTypes.JS, - code: ResizableContainerVericalSource, + code: ResizablePanelCollapsibleSource, }, { type: GuideSectionTypes.HTML, - code: ResizableContainerVericalHtml, + code: ResizablePanelCollapsibleHtml, }, ], - title: 'Vertical resizing', + title: 'Collapsible resizable panels', text: ( -

    - Set direction=vertical on{' '} - EuiResizableContainer to set a vertical orientation - of the resizable panels. -

    +
    +

    + Panels can be designated as collapsible, which allows them to hide + content and automatically resize to a minimal width. The intent of + collapsible panels is to enable large, layout-level content areas to + cede space to a main content area. For instance, collapsing an + action panel to allow more focus on the primary display panel. +

    +

    + Use the mode prop on an{' '} + EuiResizablePanel to mark it as{' '} + collapsible or main. From the + provided mode configuration, the{' '} + EuiResizableContainer will determine placement of + the toggle button and functionality of panel collapsing. To prevent + empty states, not all panels can be{' '} + mode=collapsible (there must be at least one{' '} + mode=main panel). +

    +
    ), - props: { EuiResizableContainer, EuiResizablePanel, EuiResizableButton }, - demo: ( -
    - + props: { + EuiResizableContainer, + EuiResizablePanel, + EuiResizableButton, + ModeOptions, + PanelModeType, + ToggleOptions, + }, + demo: , + snippet: collapsibleSnippet, + }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: ResizablePanelCollapsibleOptsSource, + }, + { + type: GuideSectionTypes.HTML, + code: ResizablePanelCollapsibleOptsHtml, + }, + ], + title: 'Collapsible panel options', + text: ( +
    +

    + An EuiResizablePanel marked as{' '} + {"mode={['collapsible']}"} also + accepts configuration options for the collapsible button by passing + a second parameter, in the form of: +

    + + {`mode={['collapsible', { + 'data-test-subj': 'panel-1-toggle', + className: 'panel-toggle', + position: 'top', +}]}`} +
    ), + demo: , + snippet: collapsibleOptsSnippet, }, { source: [ { type: GuideSectionTypes.JS, - code: ResizableResizerSizeSource, + code: ResizablePanelCollapsibleExtSource, }, { type: GuideSectionTypes.HTML, - code: ResizableResizerSizeHtml, + code: ResizablePanelCollapsibleExtHtml, }, ], - title: 'Resizable button spacing', + title: 'Collapsible panels with external control', text: (

    - You can control the space between panels by modifying the{' '} - size prop of the{' '} - EuiResizableButton component. The available sizes - are xl, l,{' '} - m, and s. You should avoid - using different sizes within the same{' '} - EuiResizableContainer, as shown in the demo below. + EuiResizableContainer also provides action hooks + for parent components to access internal methods, such as{' '} + EuiResizablePanel collapse toggling. The actions + are accessible via the third parameter of the render prop function. +

    +

    + Note that when bypassing internal{' '} + EuiResizableContainer logic, it is possible to + create situations that would otherwise be prevented. For instance, + allowing all panels to be collapsed creates a scenerio where your + app will need to account for empty state and accesibility in regards + to keyboard navigation. +

    +

    Custom collapse button

    +

    + You can also provide an external collapse button for custom + placement and look within your panel with{' '} + {"mode={['custom']}"}. When + collapsed, however, the default collapsed button will be used for + users to uncollapse the panel. +

    +

    + For consistency, we recommend the usage of the{' '} + menuLeft, menuRight, etc, icon + types.

    ), - props: { EuiResizableContainer, EuiResizablePanel, EuiResizableButton }, - demo: , + demo: , + snippet: collapsibleExtSnippet, }, ], }; diff --git a/src-docs/src/views/resizable_container/resizable_container_reset_values.js b/src-docs/src/views/resizable_container/resizable_container_reset_values.js index aa3ba302ee60..525bc7c56057 100644 --- a/src-docs/src/views/resizable_container/resizable_container_reset_values.js +++ b/src-docs/src/views/resizable_container/resizable_container_reset_values.js @@ -83,7 +83,7 @@ export default () => { {(EuiResizablePanel, EuiResizableButton) => ( <> @@ -92,18 +92,18 @@ export default () => { size={sizes[firstPanelId]} minSize="30%"> -

    {text}

    +
    {text}
    - + -

    {text}

    +
    {text}
    diff --git a/src-docs/src/views/resizable_container/resizable_container_three_panels.js b/src-docs/src/views/resizable_container/resizable_container_three_panels.js deleted file mode 100644 index ee37135cfe99..000000000000 --- a/src-docs/src/views/resizable_container/resizable_container_three_panels.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { EuiText, EuiResizableContainer } from '../../../../src/components'; -import { fake } from 'faker'; - -const text = fake('{{lorem.paragraphs}}'); - -export default () => ( - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - -

    {text}

    -
    -
    - - - - - -

    {text}

    -
    -
    - - - - - -

    {text}

    -
    -
    - - )} -
    -); diff --git a/src-docs/src/views/resizable_container/resizable_container_vertical.js b/src-docs/src/views/resizable_container/resizable_container_vertical.js index edc649f29ee4..5807bf771740 100644 --- a/src-docs/src/views/resizable_container/resizable_container_vertical.js +++ b/src-docs/src/views/resizable_container/resizable_container_vertical.js @@ -12,20 +12,20 @@ const text = ( ); export default () => ( - + {(EuiResizablePanel, EuiResizableButton) => ( <> -

    {text}

    +
    {text}
    - + -

    {text}

    +
    {text}
    diff --git a/src-docs/src/views/resizable_container/resizable_panel_collapsible.js b/src-docs/src/views/resizable_container/resizable_panel_collapsible.js new file mode 100644 index 000000000000..f540a3240d03 --- /dev/null +++ b/src-docs/src/views/resizable_container/resizable_panel_collapsible.js @@ -0,0 +1,164 @@ +import React, { useState } from 'react'; +import { + EuiText, + EuiResizableContainer, + EuiListGroup, + EuiListGroupItem, + EuiPanel, + EuiTitle, + EuiSpacer, + EuiPage, +} from '../../../../src/components'; +import { fake } from 'faker'; + +const texts = []; + +for (let i = 0; i < 4; i++) { + texts.push(

    {fake('{{lorem.paragraph}}')}

    ); +} + +export default () => { + const items = [ + { + id: 1, + label: 'First item', + text: texts[0], + active: true, + }, + { + id: 2, + label: 'Second item', + text: texts[1], + }, + { + id: 3, + label: 'Third item', + text: texts[2], + }, + { + id: 4, + label: 'Forth item', + text: texts[3], + }, + ]; + + const [itemSelected, setItemSelected] = useState(items[0]); + const itemElements = items.map((item, index) => ( + setItemSelected(item)} + label={item.label} + size="s" + /> + )); + + return ( + <> + +

    Simple

    +
    + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {itemElements} + + + + + + + +

    {itemSelected.label}

    +
    + + {itemSelected.text} +
    +
    + + )} +
    +
    + + + +

    Multiple collapsible panels

    +
    + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {itemElements} + + + + + + + +

    {itemSelected.label}

    +
    + + {itemSelected.text} +
    +
    + + + + + {itemElements} + + + )} +
    +
    + + + +

    Vertical collapsible panels

    +
    + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {itemElements} + + + + + + + +

    {itemSelected.label}

    +
    + + {itemSelected.text} +
    +
    + + )} +
    +
    + + ); +}; diff --git a/src-docs/src/views/resizable_container/resizable_panel_collapsible_external.js b/src-docs/src/views/resizable_container/resizable_panel_collapsible_external.js new file mode 100644 index 000000000000..bd1679baf586 --- /dev/null +++ b/src-docs/src/views/resizable_container/resizable_panel_collapsible_external.js @@ -0,0 +1,123 @@ +import React, { useRef, useState } from 'react'; +import { + EuiResizableContainer, + EuiPanel, + EuiTitle, + EuiSpacer, + EuiButtonGroup, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '../../../../src/components'; + +const toggleButtons = [ + { + id: '1', + label: 'Toggle Panel 1', + }, + { + id: '2', + label: 'Toggle Panel 2', + }, +]; + +export default () => { + const collapseFn = useRef(() => {}); + + const [toggleIdToSelectedMap, setToggleIdToSelectedMap] = useState({}); + + const onCollapse = (optionId) => { + const newToggleIdToSelectedMap = { + ...toggleIdToSelectedMap, + [optionId]: !toggleIdToSelectedMap[optionId], + }; + setToggleIdToSelectedMap(newToggleIdToSelectedMap); + }; + + const onChange = (optionId) => { + onCollapse(optionId); + collapseFn.current(`panel${optionId}`, optionId === '3' ? 'right' : 'left'); + }; + + return ( + <> +
    + +
    + + + {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { + collapseFn.current = (id, direction = 'left') => + togglePanel(id, { direction }); + return ( + <> + + + +

    Panel 1

    +
    +
    +
    + + + + + + +

    Panel 2

    +
    +
    +
    + + + + + + + + +

    Panel 3

    +
    +
    + + onChange(3)} + /> + +
    + + +

    + This panel provides its own button for triggering + collapsibility but relies on the default collapsed button + to uncollapse. +

    +
    +
    +
    + + ); + }} +
    + + ); +}; diff --git a/src-docs/src/views/resizable_container/resizable_panel_collapsible_options.js b/src-docs/src/views/resizable_container/resizable_panel_collapsible_options.js new file mode 100644 index 000000000000..8115d0465fc0 --- /dev/null +++ b/src-docs/src/views/resizable_container/resizable_panel_collapsible_options.js @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { + EuiResizableContainer, + EuiListGroup, + EuiListGroupItem, + EuiPanel, + EuiTitle, + EuiSpacer, + EuiText, + EuiPage, +} from '../../../../src/components'; +import { fake } from 'faker'; + +const texts = []; + +for (let i = 0; i < 4; i++) { + texts.push(

    {fake('{{lorem.paragraph}}')}

    ); +} + +export default () => { + const items = [ + { + id: 1, + label: 'First item', + text: texts[0], + }, + { + id: 2, + label: 'Second item', + text: texts[1], + }, + { + id: 3, + label: 'Third item', + text: texts[2], + }, + { + id: 4, + label: 'Forth item', + text: texts[3], + }, + ]; + + const [itemSelected, setItemSelected] = useState(items[0]); + const itemElements = items.map((item, index) => ( + setItemSelected(item)} + label={item.label} + size="s" + /> + )); + return ( + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {itemElements} + + + + + + + +

    {itemSelected.label}

    +
    + + {itemSelected.text} +
    +
    + + + + + {itemElements} + + + )} +
    +
    + ); +}; diff --git a/src-docs/src/views/resizable_container/resizable_panels.js b/src-docs/src/views/resizable_container/resizable_panels.js new file mode 100644 index 000000000000..19c29c5db3fe --- /dev/null +++ b/src-docs/src/views/resizable_container/resizable_panels.js @@ -0,0 +1,63 @@ +import React from 'react'; + +import { + EuiText, + EuiCode, + EuiResizableContainer, + EuiPanel, +} from '../../../../src/components'; + +export default () => ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + +

    + This EuiResizablePanel changes the background + with {'color="subdued"'}. +

    +
    +
    + + + + + +

    + This EuiResizablePanel resets most of the{' '} + EuiPanel props back to default with{' '} + {'color="plain" hasShadow borderRadius="m"'}. +

    +

    + It also adds padding to the wrapping div with{' '} + {'wrapperPadding="m"'} to maintain the scroll{' '} + inside the panel. +

    +
    +
    + + + + + + +

    + This EuiResizablePanel also changes the + background color but adds an internal EuiPanel{' '} + that will not stretch and will scroll within the{' '} + EuiResizablePanel. +

    +
    +
    +
    + + )} +
    +); diff --git a/src-docs/src/views/resizable_container/resizable_resizer_size.js b/src-docs/src/views/resizable_container/resizable_resizer_size.js deleted file mode 100644 index 0a42e0cb03c7..000000000000 --- a/src-docs/src/views/resizable_container/resizable_resizer_size.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; - -import { - EuiText, - EuiCode, - EuiResizableContainer, -} from '../../../../src/components'; - -export default () => ( - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - -

    - The EuiResizableButton to the right of this{' '} - EuiResizablePanel uses size xl -

    -
    -
    - - - - - -

    - The EuiResizableButton to the right of this{' '} - EuiResizablePanel uses size l -

    -
    -
    - - - - - -

    - The EuiResizableButton to the right of this{' '} - EuiResizablePanel uses size m, - which is the default size. -

    -
    -
    - - - - - -

    - The EuiResizableButton to the right of this{' '} - EuiResizablePanel uses size s -

    -
    -
    - - - - - -

    - This is the last EuiResizablePanel, so it is not - followed by a EuiResizableButton -

    -
    -
    - - )} -
    -); diff --git a/src/components/page/__snapshots__/page.test.tsx.snap b/src/components/page/__snapshots__/page.test.tsx.snap index 622d924aabd7..8d0b7edb54a7 100644 --- a/src/components/page/__snapshots__/page.test.tsx.snap +++ b/src/components/page/__snapshots__/page.test.tsx.snap @@ -3,7 +3,7 @@ exports[`EuiPage is rendered 1`] = `
    `; @@ -11,7 +11,7 @@ exports[`EuiPage is rendered 1`] = ` exports[`EuiPage restrict width can be set to a custom number 1`] = `
    @@ -20,7 +20,7 @@ exports[`EuiPage restrict width can be set to a custom number 1`] = ` exports[`EuiPage restrict width can be set to a custom value and measurement 1`] = `
    @@ -29,7 +29,7 @@ exports[`EuiPage restrict width can be set to a custom value and measurement 1`] exports[`EuiPage restrict width can be set to a default 1`] = `
    `; diff --git a/src/components/page/_page.scss b/src/components/page/_page.scss index 5bae268ad8a3..973a1ee53b1b 100644 --- a/src/components/page/_page.scss +++ b/src/components/page/_page.scss @@ -1,6 +1,5 @@ .euiPage { display: flex; - padding: $euiSize; background-color: $euiPageBackgroundColor; &--restrictWidth-default, @@ -15,6 +14,12 @@ @include euiBreakpoint('xs', 's') { flex-direction: column; - // padding: 0; temporarily removing + } +} + +// Uses the same values as EuiPanel +@each $modifier, $amount in $euiPanelPaddingModifiers { + .euiPage--#{$modifier} { + padding: $amount; } } diff --git a/src/components/page/page.tsx b/src/components/page/page.tsx index 9a7e077877d0..89668dd07e7b 100644 --- a/src/components/page/page.tsx +++ b/src/components/page/page.tsx @@ -19,26 +19,41 @@ import React, { FunctionComponent, HTMLAttributes } from 'react'; import classNames from 'classnames'; -import { CommonProps } from '../common'; +import { CommonProps, keysOf } from '../common'; + +const paddingSizeToClassNameMap = { + none: null, + s: 'euiPage--paddingSmall', + m: 'euiPage--paddingMedium', + l: 'euiPage--paddingLarge', +}; + +export const SIZES = keysOf(paddingSizeToClassNameMap); export interface EuiPageProps extends CommonProps, HTMLAttributes { - restrictWidth?: boolean | number | string; -} - -export const EuiPage: FunctionComponent = ({ - children, /** * Sets the max-width of the page, - * set to `true` to use the default size, + * set to `true` to use the default size of `1000px`, * set to `false` to not restrict the width, * set to a number for a custom width in px, * set to a string for a custom width in custom measurement. */ + restrictWidth?: boolean | number | string; + /** + * Adjust the padding. + * When using this setting it's best to be consistent throught all similar usages. + */ + paddingSize?: typeof SIZES[number]; +} + +export const EuiPage: FunctionComponent = ({ + children, restrictWidth = false, style, className, + paddingSize = 'm', ...rest }) => { let widthClassname; @@ -51,7 +66,12 @@ export const EuiPage: FunctionComponent = ({ newStyle = { ...style, maxWidth: restrictWidth }; } - const classes = classNames('euiPage', widthClassname, className); + const classes = classNames( + 'euiPage', + widthClassname, + paddingSizeToClassNameMap[paddingSize], + className + ); return (
    diff --git a/src/components/panel/index.ts b/src/components/panel/index.ts index b7419dcafed6..829cad7ff273 100644 --- a/src/components/panel/index.ts +++ b/src/components/panel/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { EuiPanel, PanelPaddingSize, SIZES } from './panel'; +export { EuiPanel, EuiPanelProps, PanelPaddingSize, SIZES } from './panel'; diff --git a/src/components/panel/panel.tsx b/src/components/panel/panel.tsx index d18163566ae5..36a8feb2da42 100644 --- a/src/components/panel/panel.tsx +++ b/src/components/panel/panel.tsx @@ -29,6 +29,13 @@ import classNames from 'classnames'; import { CommonProps, keysOf, ExclusiveUnion } from '../common'; import { EuiBetaBadge } from '../badge/beta_badge'; +export const panelPaddingValues = { + none: 0, + s: 8, + m: 16, + l: 24, +}; + const paddingSizeToClassNameMap = { none: null, s: 'euiPanel--paddingSmall', @@ -60,7 +67,7 @@ export type PanelColor = typeof COLORS[number]; export type PanelPaddingSize = typeof SIZES[number]; export type PanelBorderRadius = typeof BORDER_RADII[number]; -interface Props extends CommonProps { +export interface PanelProps extends CommonProps { /** * Adds a medium shadow to the panel; * Clickable cards will still get a shadow on hover @@ -103,11 +110,11 @@ interface Props extends CommonProps { } interface Divlike - extends Props, + extends PanelProps, Omit, 'onClick' | 'color'> {} interface Buttonlike - extends Props, + extends PanelProps, Omit, 'color'> {} export type EuiPanelProps = ExclusiveUnion; diff --git a/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap b/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap index 175467df6b48..4f96f293df93 100644 --- a/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap +++ b/src/components/resizable_container/__snapshots__/resizable_container.test.tsx.snap @@ -1,30 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`EuiResizableContainer can adjust button spacing 1`] = ` +exports[`EuiResizableContainer can adjust panel props 1`] = `
    - Testing +
    + Testing +
    `; @@ -36,24 +45,33 @@ exports[`EuiResizableContainer can be controlled externally 1`] = ` data-test-subj="test subject string" >
    - Testing +
    + Testing +
    `; @@ -65,24 +83,33 @@ exports[`EuiResizableContainer can be vertical 1`] = ` data-test-subj="test subject string" >
    - Testing +
    + Testing +
    `; @@ -94,37 +121,51 @@ exports[`EuiResizableContainer can have more than two panels 1`] = ` data-test-subj="test subject string" >
    - Testing +
    + Testing +
    `; @@ -136,24 +177,71 @@ exports[`EuiResizableContainer can have scrollable panels 1`] = ` data-test-subj="test subject string" >
    - Testing +
    + Testing +
    +`; + +exports[`EuiResizableContainer can have toggleable panels 1`] = ` +
    +
    +
    + Sidebar +
    +
    +
    `; @@ -165,24 +253,71 @@ exports[`EuiResizableContainer is rendered 1`] = ` data-test-subj="test subject string" >
    +
    + Testing +
    +
    +
    +`; + +exports[`EuiResizableContainer toggleable panels can be configurable 1`] = ` +
    +
    - Testing +
    + Sidebar +
    `; diff --git a/src/components/resizable_container/_index.scss b/src/components/resizable_container/_index.scss index 0f42947c498a..8c52f0fffbe0 100644 --- a/src/components/resizable_container/_index.scss +++ b/src/components/resizable_container/_index.scss @@ -1,4 +1,5 @@ @import 'variables'; @import 'resizable_button'; +@import 'resizable_collapse_button'; @import 'resizable_container'; @import 'resizable_panel'; diff --git a/src/components/resizable_container/_resizable_button.scss b/src/components/resizable_container/_resizable_button.scss index 212e7c8eabab..e0aac37146cc 100644 --- a/src/components/resizable_container/_resizable_button.scss +++ b/src/components/resizable_container/_resizable_button.scss @@ -3,6 +3,7 @@ .euiResizableButton { position: relative; flex-shrink: 0; + z-index: $euiZLevel1; &:before, &:after { @@ -22,11 +23,14 @@ &.euiResizableButton--horizontal { cursor: col-resize; + width: $euiResizableButtonSize; + margin-left: -($euiResizableButtonSize / 2); + margin-right: -($euiResizableButtonSize / 2); &:before, &:after { width: 1px; - height: 12px; + height: $euiSizeM; } &:before { @@ -40,10 +44,13 @@ &.euiResizableButton--vertical { cursor: row-resize; + height: $euiResizableButtonSize; + margin-top: -($euiResizableButtonSize / 2); + margin-bottom: -($euiResizableButtonSize / 2); &:before, &:after { - width: 12px; + width: $euiSizeM; height: 1px; } @@ -57,7 +64,7 @@ } //Lighten the "grab" icon on :hover - &:hover { + &:hover:not(:disabled) { &:before, &:after { background-color: $euiColorMediumShade; @@ -66,7 +73,7 @@ } //Add a transparent background to the container and emphasis the "grab" icon with primary color on :focus - &:focus { + &:focus:not(:disabled) { background-color: transparentize($euiColorPrimary, .9); &:before, @@ -83,8 +90,8 @@ } //Morph the "grab" icon into a fluid 2px straight line on :hover and :focus - &:hover, - &:focus { + &:hover:not(:disabled), + &:focus:not(:disabled) { &.euiResizableButton--horizontal { &:before, &:after { @@ -115,17 +122,8 @@ } } } -} - -//Generate modifier classes that control the size of the resizer -@each $modifier, $amount in $euiResizableButtonSizeModifiers { - .euiResizableButton--#{$modifier} { - &.euiResizableButton--vertical { - height: $amount; - } - &.euiResizableButton--horizontal { - width: $amount; - } + &:disabled { + display: none !important; // sass-lint:disable-line no-important } } diff --git a/src/components/resizable_container/_resizable_collapse_button.scss b/src/components/resizable_container/_resizable_collapse_button.scss new file mode 100644 index 000000000000..ccc6100993a4 --- /dev/null +++ b/src/components/resizable_container/_resizable_collapse_button.scss @@ -0,0 +1,152 @@ +// This file has lots of modifiers and is somewhat nesty by nature because of positioning +// sass-lint:disable nesting-depth + +/** + * 1. The default position of the button should always be `middle`, so + * those position styles aren't restricted to a class + * 2. When collpsed, the button itself is the full collapsed area and we use + * flex to align the icon within + */ + +.euiResizableToggleButton { + @include euiSlightShadow; + position: absolute; + z-index: $euiZLevel1 + 1; + // Remove animations from EuiButtonIcon because of the custom transforms + animation: none !important; // sass-lint:disable-line no-important + // Remove transition from EuiButtonIcon because of the custom transforms + transition: none !important; // sass-lint:disable-line no-important + + &, + &:hover, + &:focus { + background-color: $euiColorLightestShade; + } + + &:focus { + @include euiSlightShadowHover; + background-color: $euiColorLightShade; + } + + &-isCollapsed { + box-shadow: none; + background: transparent; + border-radius: 0; + display: flex; /* 2 */ + } + + &:not(:focus):not(:active):not(.euiResizableToggleButton-isVisible):not(.euiResizableToggleButton-isCollapsed) { + @include euiScreenReaderOnly; + } +} + +.euiResizableToggleButton--horizontal { + &.euiResizableToggleButton { + &.euiResizableToggleButton--after { + right: 0; + top: 50%; /* 1 */ + transform: translate(50%, -50%); /* 1 */ + justify-content: center; /* 1, 2 */ + + &.euiResizableToggleButton--top { + top: 0; + transform: translate(50%, $euiSize); + justify-content: flex-start; /* 2 */ + } + + &.euiResizableToggleButton--bottom { + top: auto; + bottom: 0; + transform: translate(50%, -$euiSize); + justify-content: flex-end; /* 2 */ + } + } + + &.euiResizableToggleButton--before { + left: 0; + top: 50%; /* 1 */ + transform: translate(-50%, -50%); /* 1 */ + justify-content: center; /* 1, 2 */ + + &.euiResizableToggleButton--top { + top: 0; + transform: translate(-50%, $euiSize); + justify-content: flex-start; /* 2 */ + } + + &.euiResizableToggleButton--bottom { + top: auto; + bottom: 0; + transform: translate(-50%, -$euiSize); + justify-content: flex-end; /* 2 */ + } + } + + &.euiResizableToggleButton-isCollapsed { + flex-direction: column; + top: 0 !important; // sass-lint:disable-line no-important + bottom: 0 !important; // sass-lint:disable-line no-important + transform: none !important; // sass-lint:disable-line no-important + // Give some space from the cross edges + padding-top: $euiSize; + padding-bottom: $euiSize; + } + } +} + +.euiResizableToggleButton--vertical { + &.euiResizableToggleButton { + + &.euiResizableToggleButton--after { + top: 100%; + left: 50%; /* 1 */ + transform: translate(-50%, -50%); /* 1 */ + justify-content: center; /* 1, 2 */ + + &.euiResizableToggleButton--left { + left: 0; + transform: translate($euiSize, -50%); + justify-content: flex-start; /* 2 */ + } + + &.euiResizableToggleButton--right { + left: auto; + right: 0; + transform: translate(-$euiSize, -50%); + justify-content: flex-end; /* 2 */ + } + } + + &.euiResizableToggleButton--before { + bottom: 100%; + left: 50%; /* 1 */ + transform: translate(-50%, 50%); /* 1 */ + justify-content: center; /* 1, 2 */ + + &.euiResizableToggleButton--left { + left: 0; + transform: translate($euiSize, 50%); + justify-content: flex-start; /* 2 */ + } + + &.euiResizableToggleButton--right { + left: auto; + right: 0; + transform: translate(-$euiSize, 50%); + justify-content: flex-end; /* 2 */ + } + } + + &.euiResizableToggleButton-isCollapsed { + flex-direction: row; + top: 0 !important; // sass-lint:disable-line no-important + bottom: 0 !important; // sass-lint:disable-line no-important + left: 0 !important; // sass-lint:disable-line no-important + transform: none !important; // sass-lint:disable-line no-important + width: 100%; + // Give some space from the cross edges + padding-left: $euiSize; + padding-right: $euiSize; + } + } +} diff --git a/src/components/resizable_container/_resizable_panel.scss b/src/components/resizable_container/_resizable_panel.scss index 72d872b563dd..f9c6438b41b7 100644 --- a/src/components/resizable_container/_resizable_panel.scss +++ b/src/components/resizable_container/_resizable_panel.scss @@ -1,4 +1,51 @@ .euiResizablePanel { + position: relative; +} + +@each $modifier, $amount in $euiPanelPaddingModifiers { + .euiResizablePanel--#{$modifier} { + padding: $amount; + } +} + +.euiResizablePanel__content { + height: 100%; + + // Manually remove the border for default theme + &:not([class*='plain']) { + border-width: 0; + } +} + +.euiResizablePanel__content--scrollable { @include euiScrollBar; overflow-y: auto; } + +.euiResizablePanel-isCollapsed { + overflow: hidden; + + .euiResizablePanel__content * { + display: none; + } +} + +.euiResizableContainer--horizontal { + .euiResizablePanel-isCollapsed { + min-width: 0 !important; // sass-lint:disable-line no-important + } + + .euiResizablePanel--collapsible.euiResizablePanel-isCollapsed { + min-width: $euiSizeL !important; // sass-lint:disable-line no-important + } +} + +.euiResizableContainer--vertical { + .euiResizablePanel-isCollapsed { + min-height: 0 !important; // sass-lint:disable-line no-important + } + + .euiResizablePanel--collapsible.euiResizablePanel-isCollapsed { + min-height: $euiSizeL !important; // sass-lint:disable-line no-important + } +} diff --git a/src/components/resizable_container/_variables.scss b/src/components/resizable_container/_variables.scss index 0d55002fa6dd..f4287b2f1118 100644 --- a/src/components/resizable_container/_variables.scss +++ b/src/components/resizable_container/_variables.scss @@ -1,8 +1,2 @@ -$euiResizableButtonTransitionSpeed: $euiAnimSpeedFast; - -$euiResizableButtonSizeModifiers: ( - 'sizeSmall': $euiSizeM, - 'sizeMedium': $euiSize, - 'sizeLarge': $euiSizeL, - 'sizeExtraLarge': $euiSizeXXL -) !default; \ No newline at end of file +$euiResizableButtonTransitionSpeed: $euiAnimSpeedFast !default; +$euiResizableButtonSize: $euiSize !default; diff --git a/src/components/resizable_container/context.tsx b/src/components/resizable_container/context.tsx index 27ea85a8f19d..e070645426f8 100644 --- a/src/components/resizable_container/context.tsx +++ b/src/components/resizable_container/context.tsx @@ -18,87 +18,36 @@ */ import React, { createContext, useContext } from 'react'; - -export interface EuiResizablePanelController { - id: string; - setSize: (panelSize: number) => void; - getSizePx: () => number; - minSize: string; -} - -export class EuiResizablePanelRegistry { - private panels: { [key: string]: EuiResizablePanelController } = {}; - private resizerRefs = new Set(); - - registerPanel(panel: EuiResizablePanelController) { - this.panels[panel.id] = panel; - } - - deregisterPanel(id: EuiResizablePanelController['id']) { - delete this.panels[id]; - } - - registerResizerRef(resizerRef: HTMLElement) { - this.resizerRefs.add(resizerRef); - } - - deregisterResizerRef(resizerRef: HTMLElement) { - this.resizerRefs.delete(resizerRef); - } - - getResizerSiblings(prevPanelId: string, nextPanelId: string) { - return [this.panels[prevPanelId], this.panels[nextPanelId]]; - } - - getAllResizers() { - return Array.from(this.resizerRefs); - } - - fetchAllPanels( - prevPanelId: string, - nextPanelId: string, - containerSize: number - ) { - const panelWithSizes: { [key: string]: number } = {}; - for (const key in this.panels) { - if (key !== prevPanelId && key !== nextPanelId) { - panelWithSizes[key] = - (this.panels[key].getSizePx() / containerSize) * 100; - } - } - return panelWithSizes; - } -} - -interface ContextProps { - registry?: EuiResizablePanelRegistry; +import { EuiResizableContainerRegistry } from './types'; +interface ContainerContextProps { + registry?: EuiResizableContainerRegistry; } -const EuiResizablePanelContext = createContext({}); +const EuiResizableContainerContext = createContext({}); -interface ContextProviderProps extends Required { +interface ContextProviderProps extends Required { /** * ReactNode to render as this component's content */ children: any; } -export function EuiResizablePanelContextProvider({ +export function EuiResizableContainerContextProvider({ children, registry, }: ContextProviderProps) { return ( - + {children} - + ); } -export const useEuiResizablePanelContext = () => { - const context = useContext(EuiResizablePanelContext); +export const useEuiResizableContainerContext = () => { + const context = useContext(EuiResizableContainerContext); if (!context.registry) { throw new Error( - 'useEuiResizablePanelContext must be used within a ' + 'useEuiResizableContainerContext must be used within a ' ); } return context; diff --git a/src/components/resizable_container/helpers.test.ts b/src/components/resizable_container/helpers.test.ts new file mode 100644 index 000000000000..77b9d1c9366b --- /dev/null +++ b/src/components/resizable_container/helpers.test.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pxToPercent, sizesOnly, getPanelMinSize } from './helpers'; + +import { EuiResizableContainerRegistry } from './types'; + +describe('pxToPercent', () => { + it('should convert px to percent of whole', () => { + expect(pxToPercent(10, 100)).toEqual(10); + expect(pxToPercent(0, 1000)).toEqual(0); + expect(pxToPercent(135.5, 1000)).toEqual(13.55); + expect(pxToPercent(101, 100)).toEqual(101); + expect(pxToPercent(10, 0)).toEqual(0); + expect(pxToPercent(10, -1)).toEqual(0); + expect(pxToPercent(-1, 100)).toEqual(0); + }); +}); + +describe('sizesOnly', () => { + it('reduce to sizes values only', () => { + const panels = [ + { id: '1', size: 10 }, + { id: '2', size: 20 }, + { id: '3', size: 30 }, + { id: '4', size: 0 }, + ].reduce((out: EuiResizableContainerRegistry['panels'], panel) => { + out[panel.id] = { + getSizePx: () => 10, + minSize: ['0px', '0px'], + isCollapsed: false, + prevSize: 0, + position: 'middle', + ...panel, + }; + return out; + }, {}); + expect(sizesOnly(panels)).toEqual({ '1': 10, '2': 20, '3': 30, '4': 0 }); + }); +}); + +describe('getPanelMinSize', () => { + it('should return the larger of the two, as a percentage of the whole', () => { + expect(getPanelMinSize(['10px', '10%'], 100)).toEqual(10); + expect(getPanelMinSize(['10px', '20%'], 100)).toEqual(20); + expect(getPanelMinSize(['30px', '29%'], 100)).toEqual(30); + expect(getPanelMinSize(['99px', '95%'], 100)).toEqual(99); + expect(getPanelMinSize(['50px', '50%'], 150)).toEqual(50); + }); +}); diff --git a/src/components/resizable_container/helpers.ts b/src/components/resizable_container/helpers.ts index fc008d8115dd..4577cb132683 100644 --- a/src/components/resizable_container/helpers.ts +++ b/src/components/resizable_container/helpers.ts @@ -17,230 +17,606 @@ * under the License. */ -import { useCallback, MouseEvent, TouchEvent } from 'react'; +import { + useMemo, + useReducer, + MouseEvent as ReactMouseEvent, + TouchEvent as ReactTouchEvent, +} from 'react'; -import { keys } from '../../services'; +import { assertNever } from '../common'; import { - EuiResizableButtonMouseEvent, - EuiResizableButtonKeyDownEvent, -} from './resizable_button'; -import { EuiResizablePanelRegistry } from './context'; -import { EuiResizableContainerState } from './resizable_container'; + EuiResizablePanelController, + EuiResizableButtonController, + EuiResizableContainerRegistry, + EuiResizableContainerState, + EuiResizableContainerAction, + EuiResizableContainerActions, + ActionDragStart, + ActionDragMove, + ActionKeyMove, + ActionToggle, + ActionFocus, +} from './types'; interface Params { - isHorizontal: boolean; - state: EuiResizableContainerState; - setState: React.Dispatch>; + initialState: EuiResizableContainerState; containerRef: React.RefObject; - registryRef: React.MutableRefObject; onPanelWidthChange?: ({}: { [key: string]: number }) => any; } -type onMouseMove = (event: MouseEvent | TouchEvent) => void; - -function isMouseEvent(event: MouseEvent | TouchEvent): event is MouseEvent { +function isMouseEvent( + event: ReactMouseEvent | ReactTouchEvent +): event is ReactMouseEvent { return typeof event === 'object' && 'pageX' in event && 'pageY' in event; } -const pxToPercent = (proportion: number, whole: number) => - (proportion / whole) * 100; +export const pxToPercent = (proportion: number, whole: number) => { + if (whole < 1 || proportion < 0) return 0; + return (proportion / whole) * 100; +}; -const getPanelMinSize = ( - panelMinSize: string, - containerSize: number, - resizerSize: number +export const sizesOnly = ( + panelObject: EuiResizableContainerRegistry['panels'] ) => { + return Object.values(panelObject).reduce( + (out: { [key: string]: number }, panel) => { + out[panel.id] = panel.size; + return out; + }, + {} + ); +}; + +const _getPanelMinSize = (panelMinSize: string, containerSize: number) => { let panelMinSizePercent = 0; const panelMinSizeInt = parseInt(panelMinSize); if (panelMinSize.indexOf('px') > -1) { panelMinSizePercent = pxToPercent(panelMinSizeInt, containerSize); } else if (panelMinSize.indexOf('%') > -1) { - panelMinSizePercent = - panelMinSizeInt + (resizerSize / containerSize) * panelMinSizeInt; + panelMinSizePercent = pxToPercent( + containerSize * (panelMinSizeInt / 100), + containerSize + ); } return panelMinSizePercent; }; +export const getPanelMinSize = ( + panelMinSize: string[], + containerSize: number +) => { + const paddingMin = _getPanelMinSize(panelMinSize[1], containerSize); + const configMin = _getPanelMinSize(panelMinSize[0], containerSize); + return Math.max(configMin, paddingMin); +}; + +export const getPosition = ( + event: ReactMouseEvent | ReactTouchEvent, + isHorizontal: boolean +) => { + const clientX = isMouseEvent(event) + ? event.clientX + : event.touches[0].clientX; + const clientY = isMouseEvent(event) + ? event.clientY + : event.touches[0].clientY; + return isHorizontal ? clientX : clientY; +}; + +const getSiblingPanel = ( + element: HTMLElement | null, + adjacency: 'prev' | 'next' +) => { + if (!element) return null; + const method = + adjacency === 'prev' ? 'previousElementSibling' : 'nextElementSibling'; + let sibling = element[method]; + while (sibling) { + if ( + sibling.matches('.euiResizablePanel:not(.euiResizablePanel-isCollapsed)') + ) { + return sibling; + } + sibling = sibling[method]; + } +}; + +// lazy initialization to prevent rerender on initial interaction +const init = (state: EuiResizableContainerState) => state; + export const useContainerCallbacks = ({ - isHorizontal, - state, - setState, + initialState, containerRef, - registryRef, onPanelWidthChange, -}: Params) => { - const getContainerSize = useCallback(() => { - return isHorizontal - ? containerRef.current!.getBoundingClientRect().width - : containerRef.current!.getBoundingClientRect().height; - }, [containerRef, isHorizontal]); - - const getResizerButtonsSize = useCallback(() => { - // get sum of all of resizer button sizes to proper calculate panels ratio - const allResizers = registryRef.current.getAllResizers(); - return allResizers.reduce( - (size, resizer) => - size + (isHorizontal ? resizer.offsetWidth : resizer.offsetHeight), - 0 - ); - }, [registryRef, isHorizontal]); - - const onMouseDown = useCallback( - (event: EuiResizableButtonMouseEvent) => { - const currentTarget = event.currentTarget; - const clientX = isMouseEvent(event) - ? event.clientX - : event.touches[0].clientX; - const clientY = isMouseEvent(event) - ? event.clientY - : event.touches[0].clientY; - setState((prevState) => ({ - ...prevState, - isDragging: true, - currentResizerPos: isHorizontal ? clientX : clientY, - previousPanelId: currentTarget.previousElementSibling!.id, - nextPanelId: currentTarget.nextElementSibling!.id, - resizersSize: getResizerButtonsSize(), - })); - }, - [getResizerButtonsSize, isHorizontal, setState] - ); +}: Params): [EuiResizableContainerActions, EuiResizableContainerState] => { + function reducer( + state: EuiResizableContainerState, + action: EuiResizableContainerAction + ): EuiResizableContainerState { + const getContainerSize = () => { + return state.isHorizontal + ? containerRef.current!.getBoundingClientRect().width + : containerRef.current!.getBoundingClientRect().height; + }; + + const runSideEffect = (panels: EuiResizableContainerState['panels']) => { + if (onPanelWidthChange) { + onPanelWidthChange(sizesOnly(panels)); + } + }; - const onKeyDown = useCallback( - (event: EuiResizableButtonKeyDownEvent) => { - 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 prevPanelId = currentTarget.previousElementSibling!.id; - const nextPanelId = currentTarget.nextElementSibling!.id; - - if ( - (shouldResizeHorizontalPanel || shouldResizeVerticalPanel) && - prevPanelId && - nextPanelId - ) { - event.preventDefault(); - - const { current: registry } = registryRef; - const [prevPanel, nextPanel] = registry.getResizerSiblings( + const withSideEffect = (newState: EuiResizableContainerState) => { + runSideEffect(newState.panels); + return newState; + }; + + switch (action.type) { + case 'EUI_RESIZABLE_CONTAINER_INIT': { + return { + ...state, + containerSize: getContainerSize(), + }; + } + case 'EUI_RESIZABLE_PANEL_REGISTER': { + const { panel } = action.payload; + return { + ...state, + panels: { + ...state.panels, + [panel.id]: panel, + }, + }; + } + case 'EUI_RESIZABLE_PANEL_DEREGISTER': { + const { panelId } = action.payload; + return { + ...state, + panels: Object.values(state.panels).reduce( + (out: EuiResizableContainerState['panels'], panel) => { + if (panel.id !== panelId) { + out[panel.id] = panel; + } + return out; + }, + {} + ), + }; + } + case 'EUI_RESIZABLE_BUTTON_REGISTER': { + const { resizer } = action.payload; + return { + ...state, + resizers: { + ...state.resizers, + [resizer.id]: resizer, + }, + }; + } + case 'EUI_RESIZABLE_BUTTON_DEREGISTER': { + const { resizerId } = action.payload; + return { + ...state, + resizers: Object.values(state.resizers).reduce( + (out: EuiResizableContainerState['resizers'], panel) => { + if (panel.id !== resizerId) { + out[panel.id] = panel; + } + return out; + }, + {} + ), + }; + } + case 'EUI_RESIZABLE_DRAG_START': { + const { position, prevPanelId, nextPanelId } = action.payload; + return { + ...state, + isDragging: true, + currentResizerPos: position, prevPanelId, - nextPanelId - ); - const resizersSize = getResizerButtonsSize(); - const containerSize = getContainerSize(); + nextPanelId, + }; + } + case 'EUI_RESIZABLE_DRAG_MOVE': { + if (!state.isDragging) { + return state; + } + const { position, prevPanelId, nextPanelId } = action.payload; + const prevPanel = state.panels[prevPanelId]; + const nextPanel = state.panels[nextPanelId]; + const delta = position - state.currentResizerPos; + const prevPanelMin = getPanelMinSize( + prevPanel.minSize, + state.containerSize + ); + const nextPanelMin = getPanelMinSize( + nextPanel.minSize, + state.containerSize + ); const prevPanelSize = pxToPercent( - prevPanel.getSizePx() - - (key === keys.ARROW_UP || key === keys.ARROW_LEFT ? 10 : -10), - containerSize - resizersSize + prevPanel.getSizePx() + delta, + state.containerSize ); const nextPanelSize = pxToPercent( - nextPanel.getSizePx() - - (key === keys.ARROW_DOWN || key === keys.ARROW_RIGHT ? 10 : -10), - containerSize - resizersSize - ); - - setState({ ...state, isDragging: false }); - const panelObject = registry.fetchAllPanels( - prevPanelId, - nextPanelId, - containerSize - resizersSize + nextPanel.getSizePx() - delta, + state.containerSize ); - if (onPanelWidthChange) { - onPanelWidthChange({ - ...panelObject, - [prevPanelId]: prevPanelSize, - [nextPanelId]: nextPanelSize, + if (prevPanelSize >= prevPanelMin && nextPanelSize >= nextPanelMin) { + return withSideEffect({ + ...state, + currentResizerPos: position, + panels: { + ...state.panels, + [prevPanelId]: { + ...state.panels[prevPanelId], + size: prevPanelSize, + }, + [nextPanelId]: { + ...state.panels[nextPanelId], + size: nextPanelSize, + }, + }, }); } - prevPanel.setSize(prevPanelSize); - nextPanel.setSize(nextPanelSize); + + return state; } - }, - // `setState` is safe to omit from `useCallback` - // (https://reactjs.org/docs/hooks-reference.html#usestate) - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - getContainerSize, - getResizerButtonsSize, - isHorizontal, - onPanelWidthChange, - registryRef, - ] - ); + case 'EUI_RESIZABLE_KEY_MOVE': { + const { prevPanelId, nextPanelId, direction } = action.payload; + const prevPanel = state.panels[prevPanelId]; + const nextPanel = state.panels[nextPanelId]; - const onMouseMove: onMouseMove = useCallback( - (event) => { - if (state.isDragging && state.previousPanelId && state.nextPanelId) { - const clientX = isMouseEvent(event) - ? event.clientX - : event.touches[0].clientX; - const clientY = isMouseEvent(event) - ? event.clientY - : event.touches[0].clientY; - const x = isHorizontal ? clientX : clientY; - const { current: registry } = registryRef; - const [prevPanel, nextPanel] = registry.getResizerSiblings( - state.previousPanelId, - state.nextPanelId - ); - const delta = x - state.currentResizerPos; - const containerSize = getContainerSize() - state.resizersSize; const prevPanelMin = getPanelMinSize( prevPanel.minSize, - containerSize, - state.resizersSize + state.containerSize ); const nextPanelMin = getPanelMinSize( nextPanel.minSize, - containerSize, - state.resizersSize + state.containerSize ); const prevPanelSize = pxToPercent( - prevPanel.getSizePx() + delta, - containerSize + prevPanel.getSizePx() - (direction === 'backward' ? 10 : -10), + state.containerSize ); const nextPanelSize = pxToPercent( - nextPanel.getSizePx() - delta, - containerSize + nextPanel.getSizePx() - (direction === 'forward' ? 10 : -10), + state.containerSize ); - const panelObject = registry.fetchAllPanels( - state.previousPanelId, - state.nextPanelId, - containerSize - ); if (prevPanelSize >= prevPanelMin && nextPanelSize >= nextPanelMin) { - if (onPanelWidthChange) { - onPanelWidthChange({ - ...panelObject, - [state.previousPanelId]: prevPanelSize, - [state.nextPanelId]: nextPanelSize, - }); + return withSideEffect({ + ...state, + isDragging: false, + panels: { + ...state.panels, + [prevPanelId]: { + ...state.panels[prevPanelId], + size: prevPanelSize, + }, + [nextPanelId]: { + ...state.panels[nextPanelId], + size: nextPanelSize, + }, + }, + }); + } + + return state; + } + + case 'EUI_RESIZABLE_TOGGLE': { + const { options, panelId: currentPanelId } = action.payload; + const currentPanel = state.panels[currentPanelId]; + const shouldCollapse = !currentPanel.isCollapsed; + const panelElement = document.getElementById(currentPanelId); + const prevResizer = panelElement!.previousElementSibling; + const prevPanel = prevResizer + ? prevResizer.previousElementSibling + : null; + const nextResizer = panelElement!.nextElementSibling; + const nextPanel = nextResizer ? nextResizer.nextElementSibling : null; + + const resizersToDisable: { [id: string]: boolean | null } = {}; + if (prevResizer && prevPanel) { + resizersToDisable[prevResizer.id] = state.panels[prevPanel.id] + .isCollapsed + ? true + : shouldCollapse; + } + if (nextResizer && nextPanel) { + resizersToDisable[nextResizer.id] = state.panels[nextPanel.id] + .isCollapsed + ? true + : shouldCollapse; + } + let otherPanels: EuiResizableContainerRegistry['panels'] = {}; + if ( + prevPanel && + !state.panels[prevPanel.id].isCollapsed && + options.direction === 'right' + ) { + otherPanels[prevPanel.id] = state.panels[prevPanel.id]; + } + if ( + nextPanel && + !state.panels[nextPanel.id].isCollapsed && + options.direction === 'left' + ) { + otherPanels[nextPanel.id] = state.panels[nextPanel.id]; + } + let siblings = Object.keys(otherPanels).length; + + // A toggling sequence has occurred where an immediate sibling panel + // has not been found. We need to move more broadly through the DOM + // to find the next most suitable panel or space affordance. + // Can only occur when multiple immediate sibling panels are collapsed. + if (!siblings) { + const maybePrevPanel = getSiblingPanel(panelElement, 'prev'); + const maybeNextPanel = getSiblingPanel(panelElement, 'next'); + const validPrevPanel = maybePrevPanel + ? state.panels[maybePrevPanel.id] + : null; + const validNextPanel = maybeNextPanel + ? state.panels[maybeNextPanel.id] + : null; + // Intentional, preferential redistribution order + if (validPrevPanel && options.direction === 'right') { + otherPanels[validPrevPanel.id] = validPrevPanel; + } else if (validNextPanel && options.direction === 'left') { + otherPanels[validNextPanel.id] = validNextPanel; + } else { + if (validPrevPanel) otherPanels[validPrevPanel.id] = validPrevPanel; + if (validNextPanel) otherPanels[validNextPanel.id] = validNextPanel; } - prevPanel.setSize(prevPanelSize); - nextPanel.setSize(nextPanelSize); + siblings = Object.keys(otherPanels).length; + } - setState({ ...state, currentResizerPos: x }); + let newPanelSize = shouldCollapse + ? pxToPercent( + !currentPanel.mode ? 0 : 24, // size of the default toggle button + state.containerSize + ) + : currentPanel.prevSize; + + const delta = shouldCollapse + ? (currentPanel.size - newPanelSize) / siblings + : ((newPanelSize - currentPanel.size) / siblings) * -1; + + const collapsedPanelsSize = Object.values(state.panels).reduce( + (sum: number, panel) => { + if (panel.id !== currentPanelId && panel.isCollapsed) { + sum += panel.size; + } + return sum; + }, + 0 + ); + + // A toggling sequence has occurred where a to-be-opened panel will + // become the only open panel. Rather than reopen to its previous + // size, give it the full width, less size occupied by collapsed panels. + // Can only occur with external toggling. + if (!shouldCollapse && !siblings) { + newPanelSize = 100 - collapsedPanelsSize; } + let updatedPanels: EuiResizableContainerState['panels'] = {}; + if ( + delta < 0 && + Object.values(otherPanels).some( + (panel) => + panel.size + delta < + getPanelMinSize(panel.minSize, state.containerSize) + ) + ) { + // A toggling sequence has occurred where a to-be-opened panel is + // requesting more space than its logical sibling panel can afford. + // Rather than choose another single panel to sacrifice space, + // or try to pull proportionally from all availble panels + // (neither of which is guaranteed to prevent negative resulting widths), + // or attempt something even more complex, + // we redistribute _all_ space evenly to non-collapsed panels + // as something of a reset. + // This situation can only occur when (n-1) panels are collapsed at once + // and the most recently collapsed panel gains significant width + // during the previously occurring collapse. + // That is (largely), external toggling where the default logic has + // been negated by the lack of panel mode distinction. + otherPanels = Object.values(state.panels).reduce( + (out: EuiResizableContainerState['panels'], panel) => { + if (panel.id !== currentPanelId && !panel.isCollapsed) { + out[panel.id] = { + ...panel, + }; + } + return out; + }, + {} + ); + + newPanelSize = + (100 - collapsedPanelsSize) / (Object.keys(otherPanels).length + 1); + + updatedPanels = Object.values(otherPanels).reduce( + (out: EuiResizableContainerState['panels'], panel) => { + out[panel.id] = { + ...panel, + size: newPanelSize, + }; + return out; + }, + {} + ); + } else { + // A toggling sequence has occurred that is standard and predictable + updatedPanels = Object.values(otherPanels).reduce( + (out: EuiResizableContainerState['panels'], panel) => { + out[panel.id] = { + ...panel, + size: panel.size + delta, + }; + return out; + }, + {} + ); + } + + return withSideEffect({ + ...state, + panels: { + ...state.panels, + ...updatedPanels, + [currentPanelId]: { + ...state.panels[currentPanelId], + size: newPanelSize, + isCollapsed: shouldCollapse, + prevSize: shouldCollapse ? currentPanel.size : newPanelSize, + }, + }, + resizers: Object.values(state.resizers).reduce( + (out: EuiResizableContainerState['resizers'], resizer) => { + out[resizer.id] = { + ...resizer, + isFocused: false, + isDisabled: resizersToDisable[resizer.id] ?? resizer.isDisabled, + }; + return out; + }, + {} + ), + }); } - }, - [ - getContainerSize, - isHorizontal, - onPanelWidthChange, - registryRef, - setState, - state, - ] - ); + case 'EUI_RESIZABLE_BUTTON_FOCUS': { + const { resizerId } = action.payload; + return { + ...state, + resizers: Object.values(state.resizers).reduce( + (out: EuiResizableContainerState['resizers'], resizer) => { + out[resizer.id] = { + ...resizer, + isFocused: resizer.id === resizerId, + }; + return out; + }, + {} + ), + }; + } + case 'EUI_RESIZABLE_BUTTON_BLUR': { + return { + ...state, + resizers: Object.values(state.resizers).reduce( + (out: EuiResizableContainerState['resizers'], resizer) => { + out[resizer.id] = { + ...resizer, + isFocused: false, + }; + return out; + }, + {} + ), + }; + } + case 'EUI_RESIZABLE_RESET': { + return { + ...initialState, + panels: state.panels, + resizers: state.resizers, + containerSize: state.containerSize, + }; + } + case 'EUI_RESIZABLE_ONCHANGE': { + onPanelWidthChange!(sizesOnly(state.panels)); + return state; + } + // TODO: Implement more generic version of + // 'EUI_RESIZABLE_DRAG_MOVE' to expose to consumers + case 'EUI_RESIZABLE_RESIZE': { + return state; + } + default: + assertNever(action); + return state; + } + } + + const [reducerState, dispatch] = useReducer(reducer, initialState, init); + + const actions: EuiResizableContainerActions = useMemo(() => { + return { + reset: () => dispatch({ type: 'EUI_RESIZABLE_RESET' }), + initContainer: () => + dispatch({ + type: 'EUI_RESIZABLE_CONTAINER_INIT', + }), + registerPanel: (panel: EuiResizablePanelController) => + dispatch({ + type: 'EUI_RESIZABLE_PANEL_REGISTER', + payload: { panel }, + }), + deregisterPanel: (panelId: EuiResizablePanelController['id']) => + dispatch({ + type: 'EUI_RESIZABLE_PANEL_DEREGISTER', + payload: { panelId }, + }), + registerResizer: (resizer: EuiResizableButtonController) => + dispatch({ + type: 'EUI_RESIZABLE_BUTTON_REGISTER', + payload: { resizer }, + }), + deregisterResizer: (resizerId: EuiResizableButtonController['id']) => + dispatch({ + type: 'EUI_RESIZABLE_BUTTON_DEREGISTER', + payload: { resizerId }, + }), + dragStart: ({ + prevPanelId, + nextPanelId, + position, + }: ActionDragStart['payload']) => + dispatch({ + type: 'EUI_RESIZABLE_DRAG_START', + payload: { position, prevPanelId, nextPanelId }, + }), + dragMove: ({ + prevPanelId, + nextPanelId, + position, + }: ActionDragMove['payload']) => + dispatch({ + type: 'EUI_RESIZABLE_DRAG_MOVE', + payload: { position, prevPanelId, nextPanelId }, + }), + keyMove: ({ + prevPanelId, + nextPanelId, + direction, + }: ActionKeyMove['payload']) => + dispatch({ + type: 'EUI_RESIZABLE_KEY_MOVE', + payload: { prevPanelId, nextPanelId, direction }, + }), + togglePanel: ( + panelId: ActionToggle['payload']['panelId'], + options: ActionToggle['payload']['options'] + ) => + dispatch({ + type: 'EUI_RESIZABLE_TOGGLE', + payload: { panelId, options }, + }), + resizerFocus: (resizerId: ActionFocus['payload']['resizerId']) => + dispatch({ + type: 'EUI_RESIZABLE_BUTTON_FOCUS', + payload: { resizerId }, + }), + resizerBlur: () => + dispatch({ + type: 'EUI_RESIZABLE_BUTTON_BLUR', + }), + }; + }, []); - return { - onMouseDown, - onKeyDown, - onMouseMove, - }; + return [actions, reducerState]; }; diff --git a/src/components/resizable_container/resizable_button.tsx b/src/components/resizable_container/resizable_button.tsx index 9d29ad319ab9..4ae98c83ddb0 100644 --- a/src/components/resizable_container/resizable_button.tsx +++ b/src/components/resizable_container/resizable_button.tsx @@ -20,31 +20,34 @@ import React, { FunctionComponent, ButtonHTMLAttributes, - KeyboardEvent, MouseEvent, - TouchEvent, useCallback, + useMemo, useRef, } from 'react'; import classNames from 'classnames'; import { CommonProps } from '../common'; import { EuiI18n } from '../i18n'; -import { EuiResizablePanelRegistry } from './context'; - -export type EuiResizableButtonMouseEvent = - | MouseEvent - | TouchEvent; -export type EuiResizableButtonKeyDownEvent = KeyboardEvent; - -export type EuiResizableButtonSize = 's' | 'm' | 'l' | 'xl'; +import { htmlIdGenerator } from '../../services'; +import { useEuiResizableContainerContext } from './context'; +import { + EuiResizableButtonController, + EuiResizableButtonMouseEvent, + EuiResizableButtonKeyDownEvent, +} from './types'; interface EuiResizableButtonControls { onKeyDown: (eve: EuiResizableButtonKeyDownEvent) => void; onMouseDown: (eve: EuiResizableButtonMouseEvent) => void; onTouchStart: (eve: EuiResizableButtonMouseEvent) => void; + onFocus: (id: string) => void; + onBlur: () => void; + registration: { + register: (resizer: EuiResizableButtonController) => void; + deregister: (resizerId: EuiResizableButtonController['id']) => void; + }; isHorizontal: boolean; - registryRef: React.MutableRefObject; } export interface EuiResizableButtonProps @@ -53,35 +56,36 @@ export interface EuiResizableButtonProps keyof EuiResizableButtonControls >, CommonProps, - Partial { - /** - * The size of the Resizer (the space between panels) - */ - size?: EuiResizableButtonSize; -} + Partial {} -const sizeToClassNameMap = { - s: 'euiResizableButton--sizeSmall', - m: 'euiResizableButton--sizeMedium', - l: 'euiResizableButton--sizeLarge', - xl: 'euiResizableButton--sizeExtraLarge', -}; - -export const SIZES = Object.keys(sizeToClassNameMap); +const generatePanelId = htmlIdGenerator('resizable-button'); export const EuiResizableButton: FunctionComponent = ({ isHorizontal, className, - size = 'm', - registryRef, + id, + registration, + disabled, + onFocus, + onBlur, ...rest }) => { + const resizerId = useRef(id || generatePanelId()); + const { + registry: { resizers } = { resizers: {} }, + } = useEuiResizableContainerContext(); + const isDisabled = useMemo( + () => + disabled || + (resizers[resizerId.current] && resizers[resizerId.current].isDisabled), + [resizers, disabled] + ); const classes = classNames( 'euiResizableButton', - size ? sizeToClassNameMap[size] : null, { 'euiResizableButton--vertical': !isHorizontal, 'euiResizableButton--horizontal': isHorizontal, + 'euiResizableButton--disabled': isDisabled, }, className ); @@ -89,22 +93,33 @@ export const EuiResizableButton: FunctionComponent = ({ const previousRef = useRef(); const onRef = useCallback( (ref: HTMLElement | null) => { + if (!registration) return; + const id = resizerId.current; if (ref) { previousRef.current = ref; - registryRef!.current.registerResizerRef(ref); + registration.register({ + id, + ref, + isFocused: false, + isDisabled: disabled || false, + }); } else { if (previousRef.current != null) { - registryRef!.current.deregisterResizerRef(previousRef.current); + registration.deregister(id); previousRef.current = undefined; } } }, - [registryRef] + [registration, disabled] ); const setFocus = (e: MouseEvent) => e.currentTarget.focus(); + const handleFocus = () => { + onFocus && onFocus(resizerId.current); + }; + return ( = ({ ]}> {([horizontalResizerAriaLabel, verticalResizerAriaLabel]: string[]) => (