Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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 />,
},
{
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';

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 = (
<>
<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);
cy.get('[data-test-subj="navSpecButton"]')
.realClick()
.then(() => {
cy.get('.euiOverlayMask')
.realClick()
.then(() => {
expect(cy.get('#navSpec').should('not.exist'));
});
});
});

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'));
});
});
});
});
});
});
11 changes: 11 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,19 @@ 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'] = {
..._focusTrapProps,
shards: [buttonRef, ...(_focusTrapProps.shards || [])],
};

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

const flyout = (
Expand All @@ -151,6 +161,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