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

Add support for "isOpen" prop on popup #86

Merged
merged 5 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
## Testing Process

### Unit Testing

We use Jest as our framework for unit tests. The jest tests files follow the pattern `**.test.ts(x)`. Execute the unit tests with the following command:

```
npm run test
```

### Test Site
To facilitate manual verification, a React test site has been setup in `/test-site`. There is an App component with `ChatPanel` and `ChatPopUp` components configured.

To facilitate manual verification, a React test site has been setup in `/test-site`. There is an App component with `ChatPanel` and `ChatPopUp` components configured.

To set up the test site, make sure you have a `.env` file configured following the `.sample.env` file. Then, run the following commands:

```
npm i
npm run start
```

### Storybook

chat-ui-react also support a component preview site, power by storybook framework. Each preview, or story, is defined under the `**.stories.tsx` files in the `/tests` folder.

To view storybook site locally, run `npm run storybook`
Expand All @@ -25,14 +30,17 @@ Make sure to add stories when there's a new component or a feature update that i
## Build Process

Before initiating the build, run the linting process to identify and address any errors or warnings. Use the following command:

```
npm run lint
```

To build the library, execute:

```
npm run build
```

This will create the bundle in the `/dist` directory. This command will also generate documentation files and the `THIRD-PARTY-NOTICES` file.

For guidelines on pull request and version publish process, visit Chat SDK wiki page.
For guidelines on pull request and version publish process, visit Chat SDK wiki page.
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yext/chat-ui-react",
"version": "0.11.3",
"version": "0.11.4",
"description": "A library of React Components for powering Yext Chat integrations.",
"author": "[email protected]",
"main": "./lib/commonjs/src/index.js",
Expand Down
3 changes: 2 additions & 1 deletion src/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const builtInCssClasses: ChatPanelCssClasses = withStylelessCssClasses(
{
container: "h-full w-full flex flex-col relative shadow-2xl bg-white",
messagesScrollContainer: "flex flex-col mt-auto overflow-hidden",
messagesContainer: "flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3",
messagesContainer:
"flex flex-col gap-y-1 px-4 overflow-auto [&>*:first-child]:mt-3",
inputContainer: "w-full p-4",
messageBubbleCssClasses: {
topContainer: "mt-1",
Expand Down
112 changes: 80 additions & 32 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,7 @@ 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 +169,15 @@ export function ChatPopUp(props: ChatPopUpProps) {
const isOpenOnLoad =
(messages.length > 1 && openOnLoadLocalStorage === "true") || openOnLoad;

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);
}
}, [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,
};
}
51 changes: 34 additions & 17 deletions test-site/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ChatHeader, ChatPanel, ChatPopUp } from "@yext/chat-ui-react";
import { ChatHeader, ChatPanel, ChatPopUp, ChatPopUpProps } from "@yext/chat-ui-react";
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 +15,7 @@ const config: HeadlessConfig = {
analyticsConfig: {
endpoint: "https://www.dev.us.yextevents.com/accounts/me/events",
},
saveToLocalStorage: true,
};

function App() {
Expand All @@ -35,24 +37,39 @@ 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
Loading