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

[EuiTour] Allow attaching tour steps via HTMLElement or DOM selector #5696

Merged
merged 19 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- Added `compressed` prop to `EuiFilterGroup` and reduced the size of the `EuiFilterButton` notification badge ([#5717](https://github.com/elastic/eui/pull/5717))
- Updated `testenv` mock for `EuiIcon` to render `aria-label` as text ([#5709](https://github.com/elastic/eui/pull/5709))
- Added `editorChecklist` glyph to `EuiIcon` ([#5705](https://github.com/elastic/eui/pull/5705))
- Added `anchor` prop to `EuiTourStep` to allow for DOM selector attachment ([#5696](https://github.com/elastic/eui/pull/5696))

**Breaking changes**

Expand Down
71 changes: 71 additions & 0 deletions src-docs/src/views/tour/step_dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { useRef, useState } from 'react';

import {
EuiButtonIcon,
EuiText,
EuiSpacer,
EuiTourStep,
EuiCode,
} from '../../../../src/components';

export default () => {
const [isOpenRef, setIsOpenRef] = useState(true);
const [isOpenSelector, setIsOpenSelector] = useState(true);
const anchorRef = useRef();
return (
<div>
<EuiTourStep
anchor={() => anchorRef.current}
content={
<EuiText>
<p>
Popover is attached to the <EuiCode>anchorRef</EuiCode> button
</p>
</EuiText>
}
isStepOpen={isOpenRef}
minWidth={300}
onFinish={() => setIsOpenRef(false)}
step={1}
stepsTotal={1}
title="React ref as anchor location"
anchorPosition="rightDown"
/>
<EuiButtonIcon
onClick={() => setIsOpenRef(!isOpenRef)}
iconType="globe"
aria-label="Anchor"
buttonRef={anchorRef}
/>

<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />

<EuiTourStep
anchor="#anchorTarget"
content={
<EuiText>
<p>
Popover is attached to the <EuiCode>#anchorTarget</EuiCode> button
</p>
</EuiText>
}
isStepOpen={isOpenSelector}
minWidth={300}
onFinish={() => setIsOpenSelector(false)}
step={1}
stepsTotal={1}
title="DOM selector as anchor location"
anchorPosition="rightUp"
/>
<EuiButtonIcon
onClick={() => setIsOpenSelector(!isOpenSelector)}
iconType="globe"
aria-label="Anchor"
id="anchorTarget"
/>
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
</div>
);
};
22 changes: 22 additions & 0 deletions src-docs/src/views/tour/tour_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../../../src/components';

import Step from './step';
import StepDom from './step_dom';
import Tour from './tour';
import Managed from './managed';
import ManagedHook from './managed_hook';
Expand Down Expand Up @@ -39,6 +40,7 @@ const stepSnippet = `
</EuiText>
</EuiTourStep>
`;
const stepDomSource = require('!!raw-loader!./step_dom');
const tourSource = require('!!raw-loader!./tour');
const managedSource = require('!!raw-loader!./managed');
const managedHookSource = require('!!raw-loader!./managed_hook');
Expand Down Expand Up @@ -91,6 +93,26 @@ export const TourExample = {
demo: <Step />,
snippet: stepSnippet,
},
{
source: [
{
type: GuideSectionTypes.JS,
code: stepDomSource,
},
],
text: (
<>
<h3>Using DOM selector as anchor location</h3>
<p>
Instead of wrapping the target element, use the{' '}
<EuiCode>anchor</EuiCode> prop to specify a DOM node. Accepted
values include an HTML element, a function returning an HTML
element, or a DOM query selector.
</p>
</>
),
demo: <StepDom />,
},
{
title: 'Standalone steps',
source: [
Expand Down
15 changes: 3 additions & 12 deletions src/components/focus_trap/focus_trap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import { FocusOn } from 'react-focus-on';
import { ReactFocusOnProps } from 'react-focus-on/dist/es5/types';

import { CommonProps } from '../common';
import { findElementBySelectorOrRef, ElementTarget } from '../../services';

/**
* A DOM node, a selector string (which will be passed to
* `document.querySelector()` to find the DOM node), or a function that
* returns a DOM node.
*/
export type FocusTarget = HTMLElement | string | (() => HTMLElement);
export type FocusTarget = ElementTarget;

interface EuiFocusTrapInterface {
/**
Expand Down Expand Up @@ -70,12 +66,7 @@ export class EuiFocusTrap extends Component<EuiFocusTrapProps, State> {

// Programmatically sets focus on a nested DOM node; optional
setInitialFocus = (initialFocus?: FocusTarget) => {
let node = initialFocus instanceof HTMLElement ? initialFocus : null;
if (typeof initialFocus === 'string') {
node = document.querySelector(initialFocus as string);
} else if (typeof initialFocus === 'function') {
node = (initialFocus as () => HTMLElement)();
}
const node = findElementBySelectorOrRef(initialFocus);
if (!node) return;
// `data-autofocus` is part of the 'react-focus-on' API
node.setAttribute('data-autofocus', 'true');
Expand Down
45 changes: 45 additions & 0 deletions src/components/tour/tour_step.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.
*/

import React from 'react';

import { EuiTourStep } from './tour_step';

const steps = [
{
step: 1,
content: 'You are here',
},
];

const config = {
onFinish: () => {},
stepsTotal: 1,
title: 'A demo',
};

describe('EuiTourStep', () => {
describe('with an `anchor` configuration', () => {
it('attaches to the anchor element', () => {
cy.realMount(
<>
<span id="anchor">Test</span>
<EuiTourStep
data-test-subj="step"
{...config}
{...steps[0]}
isStepOpen
anchor="#anchor"
/>
</>
);

expect(cy.get('[data-test-subj="step"]').find('#anchor')).to.exist;
});
});
});
Loading