[cloud][chat] Apply z-index; provide button to hide (#124581)

* [cloud][chat] Apply z-index; provide button to hide

* Address feedback, cleanup

* Addressing feedback

* Adding focus selector

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2022-02-10 12:59:10 -06:00 committed by GitHub
parent bc01e77423
commit 71ab6de626
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 250 additions and 94 deletions

View file

@ -5,7 +5,15 @@
* 2.0.
*/
import React from 'react';
import React, { useState } from 'react';
import {
EuiButton,
EuiGlobalToastList,
EuiGlobalToastListProps,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { forceReRender } from '@storybook/react';
import { Chat } from './chat';
@ -15,6 +23,58 @@ export default {
parameters: {},
};
export const Component = () => {
return <Chat />;
const Toaster = () => {
const [toasts, setToasts] = useState<EuiGlobalToastListProps['toasts']>([]);
return (
<>
<EuiButton
onClick={() =>
setToasts([
{
id: 'toast',
title: 'Download complete!',
color: 'success',
text: <p>Thanks for your patience!</p>,
},
])
}
>
Show Toast
</EuiButton>
<EuiGlobalToastList
toasts={toasts}
toastLifeTimeMs={3000}
dismissToast={() => {
setToasts([]);
}}
/>
</>
);
};
export const Component = () => {
const [isHidden, setIsHidden] = useState(false);
return (
<>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<Toaster />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
setIsHidden(false);
forceReRender();
}}
disabled={!isHidden}
>
Reset
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<Chat onHide={() => setIsHidden(true)} />
</>
);
};

View file

@ -5,107 +5,87 @@
* 2.0.
*/
import React, { useEffect, useRef, useState, CSSProperties } from 'react';
import React, { useRef, useState } from 'react';
import { css } from '@emotion/react';
import { useChat } from '../../services';
import { getChatContext } from './get_chat_context';
type UseChatType =
| { enabled: false }
| {
enabled: true;
src: string;
ref: React.MutableRefObject<HTMLIFrameElement | null>;
style: CSSProperties;
isReady: boolean;
};
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty } from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
const MESSAGE_READY = 'driftIframeReady';
const MESSAGE_RESIZE = 'driftIframeResize';
const MESSAGE_SET_CONTEXT = 'driftSetContext';
import { useChatConfig } from './use_chat_config';
const useChatConfig = (): UseChatType => {
const ref = useRef<HTMLIFrameElement>(null);
const chat = useChat();
const [style, setStyle] = useState<CSSProperties>({});
const [isReady, setIsReady] = useState(false);
export interface Props {
/** Handler invoked when chat is hidden by someone. */
onHide?: () => void;
/** Handler invoked when the chat widget signals it is ready. */
onReady?: () => void;
/** Handler invoked when the chat widget signals to be resized. */
onResize?: () => void;
}
useEffect(() => {
const handleMessage = (event: MessageEvent): void => {
const { current: chatIframe } = ref;
/**
* A component that will display a trigger that will allow the user to chat with a human operator,
* when the service is enabled; otherwise, it renders nothing.
*/
export const Chat = ({ onHide = () => {}, onReady, onResize }: Props) => {
const config = useChatConfig({ onReady, onResize });
const ref = useRef<HTMLDivElement>(null);
const [isClosed, setIsClosed] = useState(false);
if (
!chat.enabled ||
!chatIframe?.contentWindow ||
event.source !== chatIframe?.contentWindow
) {
return;
}
const context = getChatContext();
const { data: message } = event;
const { user: userConfig } = chat;
const { id, email, jwt } = userConfig;
switch (message.type) {
case MESSAGE_READY: {
const user = {
id,
attributes: {
email,
},
jwt,
};
chatIframe.contentWindow.postMessage(
{
type: MESSAGE_SET_CONTEXT,
data: { context, user },
},
'*'
);
setIsReady(true);
break;
}
case MESSAGE_RESIZE: {
const styles = message.data.styles || ({} as CSSProperties);
setStyle({ ...style, ...styles });
break;
}
default:
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [chat, style]);
if (chat.enabled) {
return { enabled: true, src: chat.chatURL, ref, style, isReady };
}
return { enabled: false };
};
export const Chat = () => {
const config = useChatConfig();
if (!config.enabled) {
if (!config.enabled || isClosed) {
return null;
}
const iframeStyle = css`
const { isReady, style } = config;
const { bottom, height, right } = style;
const buttonCSS = css`
bottom: calc(${bottom} + ${height});
position: fixed;
botton: 30px;
right: 30px;
visibility: ${config.isReady ? 'visible' : 'hidden'};
right: calc(${right} + ${euiThemeVars.euiSizeXS});
visibility: hidden;
`;
return <iframe css={iframeStyle} data-test-subj="floatingChatTrigger" title="chat" {...config} />;
const button = (
<EuiButtonEmpty
css={buttonCSS}
data-test-subj="cloud-chat-hide"
name="cloudChatHide"
onClick={() => {
setIsClosed(true);
onHide();
}}
size="xs"
>
{i18n.translate('xpack.cloud.chat.hideChatButtonLabel', {
defaultMessage: 'Hide chat',
})}
</EuiButtonEmpty>
);
const containerCSS = css`
bottom: ${euiThemeVars.euiSizeXL};
position: fixed;
right: ${euiThemeVars.euiSizeXL};
visibility: ${isReady ? 'visible' : 'hidden'};
z-index: ${euiThemeVars.euiZMaskBelowHeader - 1};
&:focus [name='cloudChatHide'],
&:hover [name='cloudChatHide'] {
visibility: visible;
}
`;
return (
<div css={containerCSS} ref={ref} data-test-subj="cloud-chat">
{button}
<iframe
data-test-subj="cloud-chat-frame"
title={i18n.translate('xpack.cloud.chat.chatFrameTitle', {
defaultMessage: 'Chat',
})}
{...config}
/>
</div>
);
};

View file

@ -0,0 +1,116 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useRef, useState, CSSProperties } from 'react';
import { useChat } from '../../services';
import { getChatContext } from './get_chat_context';
import { Props as ChatProps } from './chat';
type UseChatType =
| { enabled: false }
| {
enabled: true;
src: string;
ref: React.MutableRefObject<HTMLIFrameElement | null>;
style: CSSProperties;
isReady: boolean;
};
const MESSAGE_WIDGET_READY = 'driftWidgetReady';
const MESSAGE_IFRAME_READY = 'driftIframeReady';
const MESSAGE_RESIZE = 'driftIframeResize';
const MESSAGE_SET_CONTEXT = 'driftSetContext';
type ChatConfigParams = Exclude<ChatProps, 'onHide'>;
/**
* Hook which handles positioning and communication with the chat widget.
*/
export const useChatConfig = ({
onReady = () => {},
onResize = () => {},
}: ChatConfigParams): UseChatType => {
const ref = useRef<HTMLIFrameElement>(null);
const chat = useChat();
const [style, setStyle] = useState<CSSProperties>({});
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const handleMessage = (event: MessageEvent): void => {
const { current: chatIframe } = ref;
if (
!chat.enabled ||
!chatIframe?.contentWindow ||
event.source !== chatIframe?.contentWindow
) {
return;
}
const context = getChatContext();
const { data: message } = event;
const { user: userConfig } = chat;
const { id, email, jwt } = userConfig;
switch (message.type) {
// The IFRAME is ready to receive messages.
case MESSAGE_IFRAME_READY: {
const user = {
id,
attributes: {
email,
},
jwt,
};
chatIframe.contentWindow.postMessage(
{
type: MESSAGE_SET_CONTEXT,
data: { context, user },
},
'*'
);
break;
}
// Drift is attempting to resize the IFRAME based on interactions with
// its interface.
case MESSAGE_RESIZE: {
const styles = message.data.styles || ({} as CSSProperties);
setStyle({ ...style, ...styles });
// While it might appear we should set this when we receive MESSAGE_WIDGET_READY,
// we need to wait for the iframe to be resized the first time before it's considered
// *visibly* ready.
if (!isReady) {
setIsReady(true);
onReady();
}
onResize();
break;
}
// The chat widget is ready.
case MESSAGE_WIDGET_READY:
default:
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [chat, style, onReady, onResize, isReady]);
if (chat.enabled) {
return { enabled: true, src: chat.chatURL, ref, style, isReady };
}
return { enabled: false };
};