Skip to content

Commit

Permalink
Add support for "isOpen" prop on popup
Browse files Browse the repository at this point in the history
  • Loading branch information
nbramblett committed Sep 9, 2024
1 parent ba86fe2 commit 3581c0d
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 52 deletions.
6 changes: 3 additions & 3 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

The following NPM packages may be included in this product:

- @yext/[email protected].2
- @yext/chat-headless-react@0.9.1
- @yext/chat-headless@0.10.1
- @yext/[email protected].0
- @yext/chat-headless-react@0.8.0
- @yext/chat-headless@0.9.0

These packages each contain the following license and notice below:

Expand Down
13 changes: 13 additions & 0 deletions docs/chat-ui-react.chatpopupprops.isopen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/chat-ui-react](./chat-ui-react.md) &gt; [ChatPopUpProps](./chat-ui-react.chatpopupprops.md) &gt; [isOpen](./chat-ui-react.chatpopupprops.isopen.md)

## ChatPopUpProps.isOpen property

A controlled prop to open or close the panel. If provided, the prop will override the openOnLoad prop and the panel will be controlled by the parent component.

**Signature:**

```typescript
isOpen?: boolean;
```
1 change: 1 addition & 0 deletions docs/chat-ui-react.chatpopupprops.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ChatPopUpProps extends Omit<ChatHeaderProps, "showCloseButton"
| --- | --- | --- | --- |
| [ctaLabel?](./chat-ui-react.chatpopupprops.ctalabel.md) | | string | _(Optional)_ The "Call to Action" label to be displayed next to the popup button. By default, the CTA is not shown. This prop will override the "showInitialMessagePopUp" prop, if specified. |
| [customCssClasses?](./chat-ui-react.chatpopupprops.customcssclasses.md) | | [ChatPopUpCssClasses](./chat-ui-react.chatpopupcssclasses.md) | _(Optional)_ CSS classes for customizing the component styling. |
| [isOpen?](./chat-ui-react.chatpopupprops.isopen.md) | | boolean | _(Optional)_ A controlled prop to open or close the panel. If provided, the prop will override the openOnLoad prop and the panel will be controlled by the parent component. |
| [openOnLoad?](./chat-ui-react.chatpopupprops.openonload.md) | | boolean | _(Optional)_ Whether to show the panel on load. Defaults to false. |
| [openPanelButtonIcon?](./chat-ui-react.chatpopupprops.openpanelbuttonicon.md) | | JSX.Element | _(Optional)_ Custom icon for the popup button to open the panel. |
| [showHeartBeatAnimation?](./chat-ui-react.chatpopupprops.showheartbeatanimation.md) | | boolean | _(Optional)_ Whether to show a heartbeat animation on the popup button when the panel is hidden. Defaults to false. |
Expand Down
1 change: 1 addition & 0 deletions etc/chat-ui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface ChatPopUpCssClasses {
export interface ChatPopUpProps extends Omit<ChatHeaderProps, "showCloseButton" | "customCssClasses">, Omit<ChatPanelProps, "header" | "customCssClasses"> {
ctaLabel?: string;
customCssClasses?: ChatPopUpCssClasses;
isOpen?: boolean;
openOnLoad?: boolean;
openPanelButtonIcon?: JSX.Element;
showHeartBeatAnimation?: boolean;
Expand Down
114 changes: 81 additions & 33 deletions src/components/ChatPopUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export interface ChatPopUpProps
* This prop will override the "showInitialMessagePopUp" prop, if specified.
*/
ctaLabel?: string;
/**
* A controlled prop to open or close the panel. If provided, the prop
* will override the openOnLoad prop and the panel will be controlled
* by the parent component.
*/
isOpen?: boolean;
}

/**
Expand All @@ -134,6 +140,7 @@ export function ChatPopUp(props: ChatPopUpProps) {
ctaLabel,
title,
footer,
isOpen,
} = props;

const reportAnalyticsEvent = useReportAnalyticsEvent();
Expand All @@ -147,24 +154,6 @@ export function ChatPopUp(props: ChatPopUpProps) {
const [numReadMessages, setNumReadMessagesLength] = useState<number>(0);
const [numUnreadMessages, setNumUnreadMessagesLength] = useState<number>(0);

const [showInitialMessage, setshowInitialMessage] = useState(
//only show initial message popup (if specified) when CTA label is not provided
!ctaLabel && showInitialMessagePopUp
);

const onCloseInitialMessage = useCallback(() => {
setshowInitialMessage(false);
}, []);

// control CSS behavior (fade-in/out animation) on open/close state of the panel.
const [showChat, setShowChat] = useState(false);

// control the actual DOM rendering of the panel. Start rendering on first open state
// to avoid message requests immediately on load while the popup is still "hidden"
const [renderChat, setRenderChat] = useState(false);

// Set the initial value of the local storage flag for opening on load only if it doesn't already exist

if (window.localStorage.getItem(popupLocalStorageKey) === null) {
window.localStorage.setItem(
popupLocalStorageKey,
Expand All @@ -179,6 +168,16 @@ export function ChatPopUp(props: ChatPopUpProps) {
const isOpenOnLoad =
(messages.length > 1 && openOnLoadLocalStorage === "true") || openOnLoad;

// Set the initial value of the local storage flag for opening on load only if it doesn't already exist
const {
renderChat,
showChat,
showInitialMessage,
toggleChat,
closeChat,
closeInitialMessage,
} = usePanelState(isOpen, isOpenOnLoad, !ctaLabel && showInitialMessagePopUp);

// only fetch initial message when ChatPanel is closed on load (otherwise, it will be fetched in ChatPanel)
useFetchInitialMessage(
showInitialMessagePopUp ? console.error : handleError,
Expand All @@ -188,28 +187,18 @@ export function ChatPopUp(props: ChatPopUpProps) {
!isOpenOnLoad
);

useEffect(() => {
if (!renderChat && isOpenOnLoad) {
setShowChat(true);
setRenderChat(true);
setshowInitialMessage(false);
}
}, [renderChat, messages.length, isOpenOnLoad]);

const onClick = useCallback(() => {
setShowChat((prev) => !prev);
setRenderChat(true);
setshowInitialMessage(false);
toggleChat();
window.localStorage.setItem(popupLocalStorageKey, "true");
}, []);
}, [toggleChat]);

const onClose = useCallback(() => {
setShowChat(false);
closeChat();
customOnClose?.();
// consider all the messages are read while the panel was open
setNumReadMessagesLength(messages.length);
window.localStorage.setItem(popupLocalStorageKey, "false");
}, [customOnClose, messages.length]);
}, [closeChat, customOnClose, messages.length]);

useEffect(() => {
// update number of unread messages if there are new messages added while the panel is closed
Expand Down Expand Up @@ -255,7 +244,7 @@ export function ChatPopUp(props: ChatPopUpProps) {
>
{showInitialMessage && (
<InitialMessagePopUp
onClose={onCloseInitialMessage}
onClose={closeInitialMessage}
customCssClasses={cssClasses.initialMessagePopUpCssClasses}
/>
)}
Expand Down Expand Up @@ -298,3 +287,62 @@ export function ChatPopUp(props: ChatPopUpProps) {
</div>
);
}

function usePanelState(
isOpen: boolean | undefined,
isOpenOnLoad: boolean | undefined,
initialMessageVisible: boolean | undefined
) {
// control CSS behavior (fade-in/out animation) on open/close state of the panel.
const [showChat, setShowChat] = useState(false);
// control the actual DOM rendering of the panel. Start rendering on first open state
// to avoid message requests immediately on load while the popup is still "hidden"
const [renderChat, setRenderChat] = useState(false);
const [showInitialMessage, setshowInitialMessage] = useState(
initialMessageVisible
);

useEffect(() => {
if (isOpen !== undefined) {
setShowChat(isOpen);
setRenderChat(isOpen);
}
}, [renderChat, isOpen]);

useEffect(() => {
if (!renderChat && isOpenOnLoad && isOpen === undefined) {
setShowChat(true);
setRenderChat(true);
setshowInitialMessage(false);
}
}, [renderChat, isOpen, isOpenOnLoad]);

const toggleChat = useCallback(() => {
if (isOpen !== undefined) {
return;
}
setShowChat((prev) => !prev);
setRenderChat(true);
setshowInitialMessage(false);
}, [isOpen]);

const closeChat = useCallback(() => {
if (isOpen !== undefined) {
return;
}
setShowChat(false);
}, [isOpen]);

const closeInitialMessage = useCallback(() => {
setshowInitialMessage(false);
}, []);

return {
showChat,
renderChat,
showInitialMessage,
toggleChat,
closeChat,
closeInitialMessage,
};
}
47 changes: 31 additions & 16 deletions test-site/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ChatHeader, ChatPanel, ChatPopUp } from "@yext/chat-ui-react";
import { ChatPopUpProps } from "../../lib/esm/src/components/ChatPopUp";
import {
ChatHeadlessProvider,
HeadlessConfig,
} from "@yext/chat-headless-react";
import { useState } from "react";

const config: HeadlessConfig = {
botId: process.env.REACT_APP_TEST_BOT_ID || "BOT_ID_HERE",
Expand All @@ -14,6 +16,7 @@ const config: HeadlessConfig = {
analyticsConfig: {
endpoint: "https://www.dev.us.yextevents.com/accounts/me/events",
},
saveToLocalStorage: true,
};

function App() {
Expand All @@ -35,24 +38,36 @@ function App() {
linkTarget="_parent"
/>
</div>
<ControlledPopup
title="Clippy"
openOnLoad={false}
showInitialMessagePopUp={true}
showHeartBeatAnimation={true}
showUnreadNotification={true}
messageSuggestions={[
"hey how are you",
"I'm looking to order a pair of all-mountain skis",
"Who sells cheeseburgers?",
"I want to go home",
"This sucks I want a refund and also I am suing you for negligence",
]}
/>
</ChatHeadlessProvider>
</div>
<ChatHeadlessProvider config={config}>
<ChatPopUp
title="Clippy"
openOnLoad={false}
showInitialMessagePopUp={true}
showHeartBeatAnimation={true}
showUnreadNotification={true}
messageSuggestions={[
"hey how are you",
"I'm looking to order a pair of all-mountain skis",
"Who sells cheeseburgers?",
"I want to go home",
"This sucks I want a refund and also I am suing you for negligence",
]}
/>
</ChatHeadlessProvider>
<ChatHeadlessProvider config={config}></ChatHeadlessProvider>
</div>
);
}

function ControlledPopup(props: ChatPopUpProps) {
const [open, setOpen] = useState(false);

return (
<div>
<button className="bg-emerald-300 rounded-sm p-2" onClick={() => setOpen(true)}>
Open Chat
</button>
<ChatPopUp {...props} isOpen={open} onClose={() => setOpen(false)} />
</div>
);
}
Expand Down

0 comments on commit 3581c0d

Please sign in to comment.