mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
bc01e77423
commit
71ab6de626
3 changed files with 250 additions and 94 deletions
|
@ -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)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
116
x-pack/plugins/cloud/public/components/chat/use_chat_config.ts
Normal file
116
x-pack/plugins/cloud/public/components/chat/use_chat_config.ts
Normal 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 };
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue