Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiFlyout] Enable shards for scoping element clicks; Optionally delay close event #5860

Merged
merged 21 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src-docs/src/views/flyout/flyout_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const flyoutWithBannerSource = require('!!raw-loader!./flyout_banner');
import FlyoutPush from './flyout_push';
const flyoutPushSource = require('!!raw-loader!./flyout_push');

import FlyoutShards from './flyout_shards';
const flyoutShardsSource = require('!!raw-loader!./flyout_shards');

const flyOutSnippet = `<EuiFlyout onClose={closeFlyout}>
<EuiFlyoutHeader hasBorder aria-labelledby={flyoutHeadingId}>
<EuiTitle>
Expand Down Expand Up @@ -335,6 +338,33 @@ export const FlyoutExample = {
demo: <FlyoutPush />,
props: { EuiFlyout },
},
{
title: 'Passing options to EuiFocusTrap',
source: [
{
type: GuideSectionTypes.JS,
code: flyoutShardsSource,
},
],
text: (
<Fragment>
<p>
To configure certain options on the underlying{' '}
<EuiCode>EuiFocusTrap</EuiCode>, use the
<EuiCode>focusTrapProps</EuiCode> prop.
</p>
<p>
<EuiCode>shards</EuiCode> and <EuiCode>closeOnMouseup</EuiCode> each
affect how outside clicks will get handled.{' '}
<EuiCode>shards</EuiCode> specifies an array of elements that will
be considered part of the flyout, preventing close when clicked.
<EuiCode>closeOnMouseup</EuiCode> will delay the close callback,
allowing time for external toggle buttons to handle close behavior.
</p>
</Fragment>
),
demo: <FlyoutShards />,
thompsongl marked this conversation as resolved.
Show resolved Hide resolved
},
{
title: 'Understanding max-width',
source: [
Expand Down
108 changes: 108 additions & 0 deletions src-docs/src/views/flyout/flyout_shards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState, useRef } from 'react';
thompsongl marked this conversation as resolved.
Show resolved Hide resolved

import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiButton,
EuiTitle,
EuiFlyoutFooter,
EuiSpacer,
EuiText,
EuiCode,
} from '../../../../src/components';
import { useGeneratedHtmlId } from '../../../../src/services';

export default () => {
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
const [isFlyoutVisible2, setIsFlyoutVisible2] = useState(false);
const shardsFlyoutTitleId = useGeneratedHtmlId({
prefix: 'shardsFlyoutTitle',
});

const buttonRef = useRef();

let flyout;

if (isFlyoutVisible) {
flyout = (
<EuiFlyout
size="s"
onClose={() => setIsFlyoutVisible(false)}
aria-labelledby={shardsFlyoutTitleId}
ownFocus={false}
outsideClickCloses
focusTrapProps={{ shards: [buttonRef], closeOnMouseup: false }}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id={shardsFlyoutTitleId}>
<EuiCode>focusTrapProps.shards</EuiCode>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>The toggle button is considered part of this flyout.</p>
</EuiText>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={() => setIsFlyoutVisible(false)}>Close</EuiButton>
</EuiFlyoutFooter>
</EuiFlyout>
);
}

let flyout2;

if (isFlyoutVisible2) {
flyout = (
<EuiFlyout
size="s"
onClose={() => setIsFlyoutVisible2(false)}
aria-labelledby={shardsFlyoutTitleId}
ownFocus={false}
outsideClickCloses
focusTrapProps={{ closeOnMouseup: true }}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id={shardsFlyoutTitleId}>
<EuiCode>focusTrapProps.closeOnMouseup</EuiCode>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
The <EuiCode>onClose</EuiCode> callback will occur on mouseup for
outside clicks.
</p>
</EuiText>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={() => setIsFlyoutVisible2(false)}>
Close
</EuiButton>
</EuiFlyoutFooter>
</EuiFlyout>
);
}

return (
<div>
<EuiButton
buttonRef={buttonRef}
onClick={() => setIsFlyoutVisible(!isFlyoutVisible)}
>
Toggle flyout with shards
</EuiButton>
<EuiSpacer />
{flyout}
<EuiButton onClick={() => setIsFlyoutVisible2(!isFlyoutVisible2)}>
Toggle flyout using closeOnMouseup
</EuiButton>
{flyout2}
</div>
);
};
108 changes: 108 additions & 0 deletions src/components/collapsible_nav/collapsible_nav.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

/// <reference types="../../../cypress/support"/>

import React, { useState } from 'react';

import { EuiCollapsibleNav } from './collapsible_nav';
import { EuiHeader, EuiHeaderSectionItemButton } from '../header';
import { EuiIcon } from '../icon';

const childrenDefault = (
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
<>
<button data-test-subj="itemA">Item A</button>
<button data-test-subj="itemB">Item B</button>
<button data-test-subj="itemC">Item C</button>
<input data-test-subj="itemD" />
</>
);

const Nav = ({ children = childrenDefault }) => {
const [isOpen, setIsOpen] = useState(false);

return (
<EuiHeader
style={{ zIndex: 1001 }}
position="fixed"
sections={[
{
items: [
<EuiCollapsibleNav
style={{ top: 48 }}
id="navSpec"
isOpen={isOpen}
button={
<EuiHeaderSectionItemButton
data-test-subj="navSpecButton"
aria-label="Toggle main navigation"
onClick={() => setIsOpen(!isOpen)}
>
<EuiIcon type={'menu'} size="m" aria-hidden="true" />
</EuiHeaderSectionItemButton>
}
onClose={() => setIsOpen(false)}
>
{children}
</EuiCollapsibleNav>,
],
},
]}
/>
);
};

describe('EuiCollapsibleNav', () => {
describe('Elastic pattern', () => {
describe('Toggle button behavior', () => {
it('opens and closes nav when the main button is clicked', () => {
cy.mount(<Nav />);
cy.wait(400);
cy.get('[data-test-subj="navSpecButton"]')
.realClick()
.then(() => {
expect(cy.get('#navSpec').should('exist'));

cy.get('[data-test-subj="navSpecButton"]')
.realClick()
.then(() => {
expect(cy.get('#navSpec').should('not.exist'));
});
});
});

it('closes the nav when the overlay mask is clicked', () => {
cy.mount(<Nav />);
cy.wait(400);
thompsongl marked this conversation as resolved.
Show resolved Hide resolved
cy.get('[data-test-subj="navSpecButton"]')
.realClick()
.then(() => {
cy.get('.euiOverlayMask')
.realClick()
.then(() => {
expect(cy.get('#navSpec').should('not.exist'));
});
});
thompsongl marked this conversation as resolved.
Show resolved Hide resolved
});

it('closes the nav when the close button is clicked', () => {
cy.mount(<Nav />);
cy.wait(400);
cy.get('[data-test-subj="navSpecButton"]')
.realClick()
.then(() => {
cy.get('[data-test-subj="euiFlyoutCloseButton"]')
.realClick()
.then(() => {
expect(cy.get('#navSpec').should('not.exist'));
});
});
});
});
});
});
12 changes: 12 additions & 0 deletions src/components/collapsible_nav/collapsible_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import React, {
ReactElement,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
import {
useGeneratedHtmlId,
isWithinMinBreakpoint,
throttle,
useCombinedRefs,
} from '../../services';
import { EuiFlyout, EuiFlyoutProps } from '../flyout';

Expand Down Expand Up @@ -71,12 +73,20 @@ export const EuiCollapsibleNav: FunctionComponent<EuiCollapsibleNavProps> = ({
outsideClickCloses = true,
closeButtonPosition = 'outside',
paddingSize = 'none',
focusTrapProps: _focusTrapProps = {},
...rest
}) => {
const flyoutID = useGeneratedHtmlId({
conditionalId: id,
suffix: 'euiCollapsibleNav',
});
const buttonRef = useRef();
const combinedButtonRef = useCombinedRefs([button?.props.ref, buttonRef]);
const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = {
closeOnMouseup: false,
..._focusTrapProps,
shards: [buttonRef, ...(_focusTrapProps.shards || [])],
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Setting the initial state of pushed based on the `type` prop
Expand Down Expand Up @@ -136,6 +146,7 @@ export const EuiCollapsibleNav: FunctionComponent<EuiCollapsibleNavProps> = ({
onMouseUpCapture: (e: React.MouseEvent<HTMLElement>) => {
e.nativeEvent.stopImmediatePropagation();
},
ref: combinedButtonRef,
});

const flyout = (
Expand All @@ -151,6 +162,7 @@ export const EuiCollapsibleNav: FunctionComponent<EuiCollapsibleNavProps> = ({
outsideClickCloses={outsideClickCloses}
closeButtonPosition={closeButtonPosition}
paddingSize={paddingSize}
focusTrapProps={focusTrapProps}
{...rest}
// Props dependent on internal docked status
type={navIsDocked ? 'push' : 'overlay'}
Expand Down
Loading