Implement a11y improvements (#152126)

Fixes: #143878

This PR updates the guided onboarding panel to utilise a custom flyout
created based on the [example accessible custom
flyout](https://eui.elastic.co/pr_6247/#/utilities/portal#a-custom-flyout)
This commit is contained in:
claracruz 2023-03-08 13:31:29 +00:00 committed by GitHub
parent ab7c879c23
commit b686e0fa99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 717 additions and 344 deletions

View file

@ -6,8 +6,15 @@
* Side Public License, v 1.
*/
import { EuiThemeComputed } from '@elastic/eui';
import {
euiCanAnimate,
euiFlyoutSlideInRight,
euiYScrollWithShadows,
logicalCSS,
logicalCSSWithFallback,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { UseEuiTheme } from '@elastic/eui/src/services/theme/hooks';
import panelBgTop from '../../assets/panel_bg_top.svg';
import panelBgTopDark from '../../assets/panel_bg_top_dark.svg';
import panelBgBottom from '../../assets/panel_bg_bottom.svg';
@ -21,54 +28,98 @@ import panelBgBottomDark from '../../assets/panel_bg_bottom_dark.svg';
* See https://github.com/elastic/eui/issues/6241 for more details
*/
export const getGuidePanelStyles = ({
euiTheme,
euiThemeContext,
isDarkTheme,
}: {
euiTheme: EuiThemeComputed;
euiThemeContext: UseEuiTheme;
isDarkTheme: boolean;
}) => ({
setupButton: css`
margin-right: ${euiTheme.size.m};
`,
wellDoneAnimatedPrompt: css`
text-align: left;
`,
flyoutOverrides: {
flyoutHeader: css`
background: url(${isDarkTheme ? panelBgTopDark : panelBgTop}) top right no-repeat;
`,
flyoutContainer: css`
top: 55px !important;
// Unsetting bottom and height default values to create auto height
bottom: unset !important;
}) => {
const euiTheme = euiThemeContext.euiTheme;
const flyoutContainerBase = css`
position: fixed;
height: 100%;
max-height: 76vh;
max-inline-size: 480px;
max-block-size: auto;
inset-inline-end: 0;
inset-block-start: ${euiTheme.size.xxxl};
${euiCanAnimate} {
animation: ${euiFlyoutSlideInRight} ${euiTheme.animation.normal}
${euiTheme.animation.resistance};
}
@media (min-width: ${euiTheme.breakpoint.m}px) {
right: calc(${euiTheme.size.s} + 128px); // Accounting for margin on button
border-radius: 6px;
animation: euiModal 350ms cubic-bezier(0.34, 1.61, 0.7, 1);
box-shadow: none;
max-height: 76vh;
@media (max-width: ${euiTheme.breakpoint.s}px) {
right: 25px !important;
}
})
`;
return {
setupButton: css`
margin-right: ${euiTheme.size.m};
`,
flyoutBody: css`
overflow: auto;
.euiFlyoutBody__overflowContent {
width: 480px;
padding-top: 10px;
@media (max-width: ${euiTheme.breakpoint.s}px) {
width: 100%;
wellDoneAnimatedPrompt: css`
text-align: left;
`,
flyoutOverrides: {
flyoutContainer: css`
${flyoutContainerBase};
background: ${euiTheme.colors.emptyShade} url(${isDarkTheme ? panelBgTopDark : panelBgTop})
top right no-repeat;
padding: 0;
`,
flyoutContainerError: css`
${flyoutContainerBase};
padding: 24px;
`,
flyoutHeader: css`
flex-grow: 0;
padding: 16px 16px 0;
`,
flyoutHeaderError: css`
flex-grow: 0;
padding: 8px 0 0;
`,
flyoutContentWrapper: css`
display: flex;
block-size: 100%;
justify-content: space-between;
flex-direction: column;
`,
flyoutCloseButtonIcon: css`
position: absolute;
inset-block-start: ${euiTheme.size.base};
inset-inline-end: ${euiTheme.size.base};
`,
flyoutBodyWrapper: css`
${logicalCSS('height', '100%')}
${logicalCSSWithFallback('overflow-y', 'hidden')}
flex-grow: 1;
`,
flyoutBody: css`
${euiYScrollWithShadows(euiThemeContext, {
side: 'end',
})}
padding: 16px 10px 0 16px;
`,
flyoutBodyError: css`
height: 600px;
`,
flyoutStepsWrapper: css`
> li {
list-style-type: none;
}
}
`,
flyoutFooter: css`
border-radius: 0 0 6px 6px;
background: url(${isDarkTheme ? panelBgBottomDark : panelBgBottom}) 0 7px no-repeat;
padding: 24px 30px;
height: 125px;
`,
flyoutFooterLink: css`
color: ${euiTheme.colors.darkShade};
font-weight: 400;
`,
},
});
margin-inline-start: 0 !important;
`,
flyoutFooter: css`
border-radius: 0 0 6px 6px;
background: url(${isDarkTheme ? panelBgBottomDark : panelBgBottom}) 0 36px no-repeat;
padding: 24px 30px;
height: 125px;
flex-grow: 0;
`,
flyoutFooterLink: css`
color: ${euiTheme.colors.darkShade};
font-weight: 400;
`,
},
};
};

View file

@ -7,26 +7,7 @@
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiFlyoutFooter,
EuiButton,
EuiText,
EuiProgress,
EuiHorizontalRule,
EuiSpacer,
htmlIdGenerator,
EuiButtonEmpty,
EuiTitle,
EuiLink,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
EuiEmptyPrompt,
EuiImage,
} from '@elastic/eui';
import { useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -38,13 +19,11 @@ import type { GuidedOnboardingApi } from '../types';
import type { PluginState } from '../../common';
import { GuideStep } from './guide_panel_step';
import { QuitGuideModal } from './quit_guide_modal';
import { getGuidePanelStyles } from './guide_panel.styles';
import { GuideButton } from './guide_button';
import wellDoneAnimatedGif from '../../assets/well_done_animated.gif';
import wellDoneAnimatedDarkGif from '../../assets/well_done_animated_dark.gif';
import { GuidePanelFlyout } from './guide_panel_flyout';
interface GuidePanelProps {
api: GuidedOnboardingApi;
@ -65,43 +44,9 @@ const getProgress = (state?: GuideState): number => {
return 0;
};
const errorSection = (
<EuiEmptyPrompt
data-test-subj="guideErrorSection"
iconType="alert"
color="danger"
title={
<h2>
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', {
defaultMessage: 'Unable to load the guide',
})}
</h2>
}
body={
<>
<EuiText color="subdued">
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', {
defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`,
})}
</EuiText>
<EuiSpacer />
<EuiButton
iconSide="right"
onClick={() => window.location.reload()}
iconType="refresh"
color="danger"
>
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', {
defaultMessage: 'Reload',
})}
</EuiButton>
</>
}
/>
);
export const GuidePanel = ({ api, application, notifications, uiSettings }: GuidePanelProps) => {
const { euiTheme } = useEuiTheme();
const euiThemeContext = useEuiTheme();
const euiTheme = euiThemeContext.euiTheme;
const [isGuideOpen, setIsGuideOpen] = useState(false);
const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false);
const [pluginState, setPluginState] = useState<PluginState | undefined>(undefined);
@ -109,7 +54,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
const [isLoading, setIsLoading] = useState<boolean>(false);
const isDarkTheme = uiSettings.get('theme:darkMode');
const styles = getGuidePanelStyles({ euiTheme, isDarkTheme });
const styles = getGuidePanelStyles({ euiThemeContext, isDarkTheme });
const toggleGuide = () => {
setIsGuideOpen((prevIsGuideOpen) => !prevIsGuideOpen);
@ -224,23 +169,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
const stepsCompleted = getProgress(pluginState?.activeGuide);
const isGuideReadyToComplete = pluginState?.activeGuide?.status === 'ready_to_complete';
const getImageUrl = () => {
return isDarkTheme ? wellDoneAnimatedDarkGif : wellDoneAnimatedGif;
};
const backToGuidesButton = (
<EuiButtonEmpty
onClick={navigateToLandingPage}
iconSide="left"
iconType="arrowLeft"
flush="left"
color="text"
>
{i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', {
defaultMessage: 'Back to guides',
})}
</EuiButtonEmpty>
);
return (
<>
<div css={styles.setupButton}>
@ -254,228 +183,22 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
/>
</div>
{isGuideOpen && (
<EuiFlyout
ownFocus
onClose={toggleGuide}
aria-labelledby="onboarding-guide"
css={styles.flyoutOverrides.flyoutContainer}
maskProps={{ headerZindexLocation: 'above' }}
data-test-subj="guidePanel"
maxWidth={480}
>
{guideConfig && pluginState && pluginState.status !== 'error' ? (
<>
<EuiFlyoutHeader css={styles.flyoutOverrides.flyoutHeader}>
{backToGuidesButton}
<EuiTitle size="m">
<h2 data-test-subj="guideTitle">
{isGuideReadyToComplete
? i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', {
defaultMessage: 'Well done!',
})
: guideConfig.title}
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiHorizontalRule margin="s" />
</EuiFlyoutHeader>
<EuiFlyoutBody css={styles.flyoutOverrides.flyoutBody}>
<div>
{isGuideReadyToComplete && (
<>
<EuiImage
size="fullWidth"
src={getImageUrl()}
alt={i18n.translate('guidedOnboarding.dropdownPanel.wellDoneAnimatedGif', {
defaultMessage: `Guide completed animated gif`,
})}
/>
<EuiSpacer />
</>
)}
<EuiText size="m">
<p data-test-subj="guideDescription">
{isGuideReadyToComplete
? i18n.translate(
'guidedOnboarding.dropdownPanel.completeGuideFlyoutDescription',
{
defaultMessage: `You've completed the Elastic {guideName} guide. Feel free to come back to the Guides for more onboarding help or a refresher.`,
values: {
guideName: guideConfig.guideName,
},
}
)
: guideConfig.description}
</p>
</EuiText>
{guideConfig.docs && (
<>
<EuiSpacer size="l" />
<EuiText size="m">
<EuiLink external target="_blank" href={guideConfig.docs.url}>
{guideConfig.docs.text}
</EuiLink>
</EuiText>
</>
)}
{/* Progress bar should only show after the first step has been complete */}
{stepsCompleted > 0 && (
<>
<EuiSpacer size="xl" />
<EuiProgress
data-test-subj="guideProgress"
label={
isGuideReadyToComplete
? i18n.translate('guidedOnboarding.dropdownPanel.completedLabel', {
defaultMessage: 'Completed',
})
: i18n.translate('guidedOnboarding.dropdownPanel.progressLabel', {
defaultMessage: 'Progress',
})
}
value={stepsCompleted}
valueText={i18n.translate(
'guidedOnboarding.dropdownPanel.progressValueLabel',
{
defaultMessage: '{stepCount} steps',
values: {
stepCount: `${stepsCompleted} / ${guideConfig.steps.length}`,
},
}
)}
max={guideConfig.steps.length}
size="l"
/>
<EuiSpacer size="s" />
</>
)}
<EuiHorizontalRule />
{guideConfig?.steps.map((step, index) => {
const accordionId = htmlIdGenerator(`accordion${index}`)();
const stepState = pluginState?.activeGuide?.steps[index];
if (stepState) {
return (
<GuideStep
isLoading={isLoading}
accordionId={accordionId}
stepStatus={stepState.status}
stepConfig={step}
stepNumber={index + 1}
handleButtonClick={() => handleStepButtonClick(stepState, step)}
key={accordionId}
telemetryGuideId={guideConfig!.telemetryId}
/>
);
}
})}
{isGuideReadyToComplete && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isLoading}
onClick={() => completeGuide(guideConfig.completedGuideRedirectLocation)}
fill
// data-test-subj used for FS tracking and testing
data-test-subj={`onboarding--completeGuideButton--${
guideConfig!.telemetryId
}`}
>
{i18n.translate('guidedOnboarding.dropdownPanel.elasticButtonLabel', {
defaultMessage: 'Continue using Elastic',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
</EuiFlyoutBody>
<EuiFlyoutFooter css={styles.flyoutOverrides.flyoutFooter}>
<EuiFlexGroup
alignItems="center"
justifyContent="center"
gutterSize="xs"
responsive={false}
wrap
>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="questionInCircle"
iconSide="right"
href="https://cloud.elastic.co/support "
target="_blank"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="m"
>
{i18n.translate('guidedOnboarding.dropdownPanel.footer.support', {
defaultMessage: 'Need help?',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="faceHappy"
iconSide="right"
href="https://www.elastic.co/kibana/feedback"
target="_blank"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="s"
>
{i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', {
defaultMessage: 'Give feedback',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="exit"
iconSide="right"
onClick={openQuitGuideModal}
data-test-subj="quitGuideButton"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="s"
>
{i18n.translate(
'guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel',
{
defaultMessage: 'Quit guide',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
) : (
<EuiFlyoutBody>
{backToGuidesButton}
{errorSection}
</EuiFlyoutBody>
)}
</EuiFlyout>
)}
<GuidePanelFlyout
isOpen={isGuideOpen}
isLoading={isLoading}
styles={styles}
toggleGuide={toggleGuide}
isDarkTheme={isDarkTheme}
stepsCompleted={stepsCompleted}
isGuideReadyToComplete={isGuideReadyToComplete}
guideConfig={guideConfig}
navigateToLandingPage={navigateToLandingPage}
pluginState={pluginState}
handleStepButtonClick={handleStepButtonClick}
openQuitGuideModal={openQuitGuideModal}
euiTheme={euiTheme}
completeGuide={completeGuide}
/>
{isQuitGuideModalOpen && (
<QuitGuideModal

View file

@ -0,0 +1,184 @@
/*
* 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 {
EuiButton,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiLink,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding';
import { i18n } from '@kbn/i18n';
import wellDoneAnimatedDarkGif from '../../../assets/well_done_animated_dark.gif';
import { PluginState } from '../../../common';
import { GuideProgress } from './guide_progress';
import wellDoneAnimatedGif from '../../../assets/well_done_animated.gif';
import { getGuidePanelStyles } from '../guide_panel.styles';
export const GuidePanelFlyoutBody = ({
styles,
guideConfig,
isDarkTheme,
stepsCompleted,
isGuideReadyToComplete,
pluginState,
handleStepButtonClick,
isLoading,
completeGuide,
}: {
styles: ReturnType<typeof getGuidePanelStyles>;
guideConfig?: GuideConfig;
isDarkTheme: boolean;
stepsCompleted: number;
isGuideReadyToComplete: boolean;
pluginState?: PluginState;
handleStepButtonClick: (
stepState: GuideStepType,
step: StepConfig
) => Promise<{ pluginState: PluginState } | undefined>;
isLoading: boolean;
completeGuide: (
completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation']
) => Promise<void>;
}) => {
const docsLink = () => {
if (!guideConfig || !guideConfig.docs) {
return null;
}
return (
<>
<EuiSpacer size="l" />
<EuiText size="m">
<EuiLink external target="_blank" href={guideConfig.docs.url}>
{guideConfig.docs.text}
</EuiLink>
</EuiText>
</>
);
};
if (!guideConfig || !pluginState || (pluginState && pluginState.status === 'error')) {
return (
<EuiEmptyPrompt
data-test-subj="guideErrorSection"
iconType="alert"
color="danger"
title={
<h2>
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionTitle', {
defaultMessage: 'Unable to load the guide',
})}
</h2>
}
body={
<>
<EuiText color="subdued">
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionDescription', {
defaultMessage: `Wait a moment and try again. If the problem persists, contact your administrator.`,
})}
</EuiText>
<EuiSpacer />
<EuiButton
iconSide="right"
onClick={() => window.location.reload()}
iconType="refresh"
color="danger"
>
{i18n.translate('guidedOnboarding.dropdownPanel.errorSectionReloadButton', {
defaultMessage: 'Reload',
})}
</EuiButton>
</>
}
/>
);
}
if (isGuideReadyToComplete) {
return (
<>
<EuiImage
size="fullWidth"
src={isDarkTheme ? wellDoneAnimatedDarkGif : wellDoneAnimatedGif}
alt={i18n.translate('guidedOnboarding.dropdownPanel.wellDoneAnimatedGif', {
defaultMessage: `Guide completed animated gif`,
})}
/>
<EuiSpacer />
<EuiText size="m">
<p data-test-subj="guideDescription">
{i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutDescription', {
defaultMessage: `You've completed the Elastic {guideName} guide. Feel free to come back to the Guides for more onboarding help or a refresher.`,
values: {
guideName: guideConfig.guideName,
},
})}
</p>
</EuiText>
{docsLink()}
<GuideProgress
guideConfig={guideConfig}
styles={styles}
pluginState={pluginState}
isLoading={isLoading}
handleStepButtonClick={handleStepButtonClick}
isGuideReadyToComplete={isGuideReadyToComplete}
stepsCompleted={stepsCompleted}
/>
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
isLoading={isLoading}
onClick={() => completeGuide(guideConfig.completedGuideRedirectLocation)}
fill
// data-test-subj used for FS tracking and testing
data-test-subj={`onboarding--completeGuideButton--${guideConfig!.telemetryId}`}
>
{i18n.translate('guidedOnboarding.dropdownPanel.elasticButtonLabel', {
defaultMessage: 'Continue using Elastic',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
}
return (
<>
<div>
<EuiText size="m">
<p data-test-subj="guideDescription">{guideConfig.description}</p>
</EuiText>
{docsLink()}
<GuideProgress
guideConfig={guideConfig}
styles={styles}
pluginState={pluginState}
isLoading={isLoading}
handleStepButtonClick={handleStepButtonClick}
isGuideReadyToComplete={isGuideReadyToComplete}
stepsCompleted={stepsCompleted}
/>
</div>
</>
);
};

View file

@ -0,0 +1,87 @@
/*
* 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, EuiThemeComputed } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { getGuidePanelStyles } from '../guide_panel.styles';
export const GuidePanelFlyoutFooter = ({
styles,
euiTheme,
openQuitGuideModal,
}: {
styles: ReturnType<typeof getGuidePanelStyles>;
euiTheme: EuiThemeComputed;
openQuitGuideModal: () => void;
}) => {
return (
<div css={styles.flyoutOverrides.flyoutFooter}>
<EuiFlexGroup
alignItems="center"
justifyContent="center"
gutterSize="xs"
responsive={false}
wrap
>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="questionInCircle"
iconSide="right"
href="https://cloud.elastic.co/support "
target="_blank"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="m"
>
{i18n.translate('guidedOnboarding.dropdownPanel.footer.support', {
defaultMessage: 'Need help?',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="faceHappy"
iconSide="right"
href="https://www.elastic.co/kibana/feedback"
target="_blank"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="s"
>
{i18n.translate('guidedOnboarding.dropdownPanel.footer.feedback', {
defaultMessage: 'Give feedback',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="xs" color={euiTheme.colors.disabled}>
|
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="exit"
iconSide="right"
onClick={openQuitGuideModal}
data-test-subj="quitGuideButton"
css={styles.flyoutOverrides.flyoutFooterLink}
iconSize="s"
>
{i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', {
defaultMessage: 'Quit guide',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
};

View file

@ -0,0 +1,94 @@
/*
* 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, { ReactElement } from 'react';
import { EuiButtonIcon, EuiHorizontalRule, EuiSpacer, EuiTitle, keys } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { GuideConfig } from '@kbn/guided-onboarding';
import { getGuidePanelStyles } from '../guide_panel.styles';
export const GuidePanelFlyoutHeader = ({
styles,
titleId,
toggleGuide,
hasError,
isGuideReadyToComplete,
guideConfig,
backButton,
}: {
styles: ReturnType<typeof getGuidePanelStyles>;
titleId: string;
toggleGuide: () => void;
hasError: boolean;
isGuideReadyToComplete: boolean;
guideConfig?: GuideConfig;
backButton: ReactElement;
}) => {
/**
* ESC key closes CustomFlyout
*/
const onKeyDown = (event: any) => {
if (event.key === keys.ESCAPE) {
event.preventDefault();
event.stopPropagation();
toggleGuide();
}
};
const getTitle = () => {
if (isGuideReadyToComplete) {
return i18n.translate('guidedOnboarding.dropdownPanel.completeGuideFlyoutTitle', {
defaultMessage: 'Well done!',
});
}
return guideConfig ? guideConfig.title : '';
};
const closeIcon = (
<EuiButtonIcon
iconType="cross"
aria-label={i18n.translate('guidedOnboarding.dropdownPanel.closeButton.ariaLabel', {
defaultMessage: 'Close modal',
})}
onClick={toggleGuide}
onKeyDown={onKeyDown}
color="text"
css={styles.flyoutOverrides.flyoutCloseButtonIcon}
/>
);
if (hasError) {
return (
<div css={styles.flyoutOverrides.flyoutHeaderError}>
{backButton}
{closeIcon}
</div>
);
}
return (
<div css={styles.flyoutOverrides.flyoutHeader}>
<EuiSpacer size="s" />
{backButton}
<EuiSpacer size="s" />
<EuiTitle size="m">
<h2 id={titleId} data-test-subj="guideTitle">
{getTitle()}
</h2>
</EuiTitle>
{closeIcon}
<EuiHorizontalRule />
</div>
);
};

View file

@ -0,0 +1,94 @@
/*
* 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 { EuiHorizontalRule, EuiProgress, EuiSpacer, htmlIdGenerator } from '@elastic/eui';
import type { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding';
import { i18n } from '@kbn/i18n';
import { GuideStep } from '../guide_panel_step';
import type { PluginState } from '../../../common';
import { getGuidePanelStyles } from '../guide_panel.styles';
export const GuideProgress = ({
guideConfig,
styles,
pluginState,
isLoading,
stepsCompleted,
isGuideReadyToComplete,
handleStepButtonClick,
}: {
guideConfig: GuideConfig;
styles: ReturnType<typeof getGuidePanelStyles>;
pluginState: PluginState;
isLoading: boolean;
stepsCompleted: number;
isGuideReadyToComplete: boolean;
handleStepButtonClick: (stepState: GuideStepType, step: StepConfig) => void;
}) => {
const { flyoutStepsWrapper } = styles.flyoutOverrides;
return (
<>
{/* Progress bar should only show after the first step has been complete */}
{stepsCompleted > 0 && (
<>
<EuiSpacer size="xl" />
<EuiProgress
data-test-subj="guideProgress"
label={
isGuideReadyToComplete
? i18n.translate('guidedOnboarding.dropdownPanel.completedLabel', {
defaultMessage: 'Completed',
})
: i18n.translate('guidedOnboarding.dropdownPanel.progressLabel', {
defaultMessage: 'Progress',
})
}
value={stepsCompleted}
valueText={i18n.translate('guidedOnboarding.dropdownPanel.progressValueLabel', {
defaultMessage: '{stepCount} steps',
values: {
stepCount: `${stepsCompleted} / ${guideConfig.steps.length}`,
},
})}
max={guideConfig.steps.length}
size="l"
/>
<EuiSpacer size="s" />
</>
)}
<EuiHorizontalRule />
<ol css={flyoutStepsWrapper}>
{guideConfig?.steps.map((step, index) => {
const accordionId = htmlIdGenerator(`accordion${index}`)();
const stepState = pluginState?.activeGuide?.steps[index];
if (stepState) {
return (
<li key={accordionId}>
<GuideStep
isLoading={isLoading}
accordionId={accordionId}
stepStatus={stepState.status}
stepConfig={step}
stepNumber={index + 1}
handleButtonClick={() => handleStepButtonClick(stepState, step)}
telemetryGuideId={guideConfig!.telemetryId}
/>
</li>
);
}
})}
</ol>
</>
);
};

View file

@ -0,0 +1,140 @@
/*
* 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 {
EuiButtonEmpty,
EuiPanel,
EuiPortal,
EuiOverlayMask,
EuiFocusTrap,
EuiThemeComputed,
} from '@elastic/eui';
import { GuideConfig, GuideStep as GuideStepType, StepConfig } from '@kbn/guided-onboarding';
import { i18n } from '@kbn/i18n';
import { GuidePanelFlyoutHeader } from './guide_panel_flyout_header';
import { GuidePanelFlyoutBody } from './guide_panel_flyout_body';
import type { PluginState } from '../../../common';
import { GuidePanelFlyoutFooter } from './guide_panel_flyout_footer';
import { getGuidePanelStyles } from '../guide_panel.styles';
export const GuidePanelFlyout = ({
isOpen,
isDarkTheme,
toggleGuide,
isGuideReadyToComplete,
guideConfig,
styles,
navigateToLandingPage,
stepsCompleted,
pluginState,
handleStepButtonClick,
isLoading,
euiTheme,
openQuitGuideModal,
completeGuide,
}: {
isOpen: boolean;
isDarkTheme: boolean;
toggleGuide: () => void;
isGuideReadyToComplete: boolean;
guideConfig?: GuideConfig;
styles: ReturnType<typeof getGuidePanelStyles>;
navigateToLandingPage: () => void;
stepsCompleted: number;
pluginState?: PluginState;
handleStepButtonClick: (
stepState: GuideStepType,
step: StepConfig
) => Promise<{ pluginState: PluginState } | undefined>;
isLoading: boolean;
euiTheme: EuiThemeComputed;
openQuitGuideModal: () => void;
completeGuide: (
completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation']
) => Promise<void>;
}) => {
if (!isOpen) {
return null;
}
const guidePanelFlyoutTitleId = 'onboarding-guide';
const backToGuidesButton = (
<EuiButtonEmpty
onClick={navigateToLandingPage}
iconSide="left"
iconType="arrowLeft"
flush="left"
color="text"
>
{i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', {
defaultMessage: 'Back to guides',
})}
</EuiButtonEmpty>
);
const hasError = !guideConfig || !pluginState || (pluginState && pluginState.status === 'error');
const {
flyoutContentWrapper,
flyoutBody,
flyoutBodyWrapper,
flyoutContainerError,
flyoutContainer,
} = styles.flyoutOverrides;
return (
<EuiPortal>
<EuiOverlayMask>
<EuiFocusTrap onClickOutside={toggleGuide}>
<EuiPanel
data-test-subj="guidePanel"
aria-labelledby={guidePanelFlyoutTitleId}
role="dialog"
css={hasError ? flyoutContainerError : flyoutContainer}
>
<div css={flyoutContentWrapper}>
<GuidePanelFlyoutHeader
styles={styles}
titleId={guidePanelFlyoutTitleId}
toggleGuide={toggleGuide}
guideConfig={guideConfig}
isGuideReadyToComplete={isGuideReadyToComplete}
backButton={backToGuidesButton}
hasError={hasError}
/>
<div css={flyoutBodyWrapper}>
<div css={flyoutBody}>
<GuidePanelFlyoutBody
styles={styles}
guideConfig={guideConfig}
pluginState={pluginState}
handleStepButtonClick={handleStepButtonClick}
isLoading={isLoading}
isDarkTheme={isDarkTheme}
stepsCompleted={stepsCompleted}
isGuideReadyToComplete={isGuideReadyToComplete}
completeGuide={completeGuide}
/>
</div>
</div>
{!hasError && (
<GuidePanelFlyoutFooter
styles={styles}
euiTheme={euiTheme}
openQuitGuideModal={openQuitGuideModal}
/>
)}
</div>
</EuiPanel>
</EuiFocusTrap>
</EuiOverlayMask>
</EuiPortal>
);
};