diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e56bec8..1e4db28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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` @@ -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. \ No newline at end of file +For guidelines on pull request and version publish process, visit Chat SDK wiki page. diff --git a/docs/chat-ui-react.chatpopupprops.isopen.md b/docs/chat-ui-react.chatpopupprops.isopen.md new file mode 100644 index 0000000..9f870d2 --- /dev/null +++ b/docs/chat-ui-react.chatpopupprops.isopen.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [ChatPopUpProps](./chat-ui-react.chatpopupprops.md) > [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; +``` diff --git a/docs/chat-ui-react.chatpopupprops.md b/docs/chat-ui-react.chatpopupprops.md index 7f2e7f2..15bcee2 100644 --- a/docs/chat-ui-react.chatpopupprops.md +++ b/docs/chat-ui-react.chatpopupprops.md @@ -19,6 +19,7 @@ export interface ChatPopUpProps extends Omit, Omit { ctaLabel?: string; customCssClasses?: ChatPopUpCssClasses; + isOpen?: boolean; openOnLoad?: boolean; openPanelButtonIcon?: JSX.Element; showHeartBeatAnimation?: boolean; diff --git a/package-lock.json b/package-lock.json index ccc33b5..8b0b490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yext/chat-ui-react", - "version": "0.11.3", + "version": "0.11.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yext/chat-ui-react", - "version": "0.11.3", + "version": "0.11.4", "license": "BSD-3-Clause", "dependencies": { "react-markdown": "^6.0.3", diff --git a/package.json b/package.json index 925fa5d..bc9ca82 100644 --- a/package.json +++ b/package.json @@ -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": "clippy@yext.com", "main": "./lib/commonjs/src/index.js", diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 84e2f3b..a2165e2 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -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", diff --git a/src/components/ChatPopUp.tsx b/src/components/ChatPopUp.tsx index b991ace..3693e77 100644 --- a/src/components/ChatPopUp.tsx +++ b/src/components/ChatPopUp.tsx @@ -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; } /** @@ -134,6 +140,7 @@ export function ChatPopUp(props: ChatPopUpProps) { ctaLabel, title, footer, + isOpen, } = props; const reportAnalyticsEvent = useReportAnalyticsEvent(); @@ -147,24 +154,7 @@ export function ChatPopUp(props: ChatPopUpProps) { const [numReadMessages, setNumReadMessagesLength] = useState(0); const [numUnreadMessages, setNumUnreadMessagesLength] = useState(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, @@ -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, @@ -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 @@ -255,7 +244,7 @@ export function ChatPopUp(props: ChatPopUpProps) { > {showInitialMessage && ( )} @@ -298,3 +287,62 @@ export function ChatPopUp(props: ChatPopUpProps) { ); } + +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, + }; +} diff --git a/test-site/src/App.tsx b/test-site/src/App.tsx index 4fc9b63..e84f56d 100644 --- a/test-site/src/App.tsx +++ b/test-site/src/App.tsx @@ -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", @@ -14,6 +15,7 @@ const config: HeadlessConfig = { analyticsConfig: { endpoint: "https://www.dev.us.yextevents.com/accounts/me/events", }, + saveToLocalStorage: true, }; function App() { @@ -35,24 +37,39 @@ function App() { linkTarget="_parent" /> + - - - + + + ); +} + +function ControlledPopup(props: ChatPopUpProps) { + const [open, setOpen] = useState(false); + + return ( +
+ + setOpen(false)} />
); }