From cde951b20fc1aeea999c545fe4e47a1797d31cf2 Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Fri, 8 Sep 2023 10:21:28 -0400
Subject: [PATCH 01/39] NewsletterSignup as Refactored FeedbackBox
---
.gitignore | 3 +-
.../NewsletterSignup/NewsletterSignup.mdx | 385 +++++++++++++
.../NewsletterSignup.stories.tsx | 104 ++++
.../NewsletterSignup.test.tsx | 334 +++++++++++
.../NewsletterSignup/NewsletterSignup.tsx | 524 ++++++++++++++++++
.../__snapshots__/FeedbackBox.test.tsx.snap | 17 +
.../useNewsletterSignupReducer.ts | 66 +++
src/theme/components/newsletterSignup.ts | 70 +++
src/theme/index.ts | 2 +
9 files changed, 1504 insertions(+), 1 deletion(-)
create mode 100644 src/components/NewsletterSignup/NewsletterSignup.mdx
create mode 100644 src/components/NewsletterSignup/NewsletterSignup.stories.tsx
create mode 100644 src/components/NewsletterSignup/NewsletterSignup.test.tsx
create mode 100644 src/components/NewsletterSignup/NewsletterSignup.tsx
create mode 100644 src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
create mode 100644 src/components/NewsletterSignup/useNewsletterSignupReducer.ts
create mode 100644 src/theme/components/newsletterSignup.ts
diff --git a/.gitignore b/.gitignore
index b3feec8272..d8dc66c698 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,5 @@ lib
dist
.vscode
.eslintcache
-coverage
\ No newline at end of file
+coverage
+.idea
diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx
new file mode 100644
index 0000000000..57d290b20f
--- /dev/null
+++ b/src/components/NewsletterSignup/NewsletterSignup.mdx
@@ -0,0 +1,385 @@
+import { ArgTypes, Canvas, Description, Meta, Source } from "@storybook/blocks";
+
+import * as NewsletterSignupStories from "./NewsletterSignup.stories";
+import Link from "../Link/Link";
+
+
+
+# NewsletterSignup
+
+| Component Version | DS Version |
+| ----------------- | ---------- |
+| Added | `1.3.0` |
+| Latest | `1.5.3` |
+
+## Table of Contents
+
+- {Overview}
+- {Component Props}
+- {Accessibility}
+- {Notification Text}
+- {Form Fields}
+- {NewsletterSignup Screens}
+- {Form Submission Data}
+- {Programmatically Open}
+
+## Overview
+
+
+
+An NYPL privacy policy link will render within every screen of the NewsletterSignup
+("form", "confirmation", and "error" ), and the link will open in a new tab.
+
+**Note**: For the purposes of Storybook, only one (1) `NewsletterSignup` component
+example is rendered on the bottom right of this page. The `NewsletterSignup` example
+below alternately renders the "confirmation" and "error" screens on each form
+submission. This is just to demonstrate the different states of the component.
+In practice, the consuming app is responsible for handling the form submission.
+
+## Component Props
+
+
+
+
+
+## Accessibility
+
+The `NewsletterSignup` component is a complex component built from various Reservoir
+DS and Chakra components. The two main components are the DS `Button` component
+used to open Chakra's `Drawer` component.
+
+When the primary button is clicked, the dialog opens and focus is set to the
+first focusable element which is the "close" button that contains minus icon in
+the header of the dialog. While opened, focus is trapped within the dialog until
+it is closed either by clicking on the "close" or "Cancel" buttons, pressing the
+"Escape" key, or by clicking outside of the dialog. When the `NewsletterSignup`
+component is closed, focus is set back to the primary button that opened the
+dialog.
+
+The markup of the `NewsletterSignup` structurally matches the modal dialog pattern
+that is implemented by Chakra's `Modal` and `Drawer` components. The container
+has `role=”dialog”`, `aria-modal=”true”`, `tabindex={0}`, `aria-labelledby` that
+references the title within the dialog, and `aria-describedby` that references a
+descriptive piece of text within the dialog.
+
+Within the `NewsletterSignup` component, the radio input field is created from the DS
+`RadioGroup` and `Radio` components, and input fields are created from the DS
+`TextInput` component. Each of these components has their own accessibility
+features documented in their respective Storybook pages.
+
+When the form is submitted, focus is set to the confirmation message or the
+error message if an error occurs.
+
+Whereas the `NewsletterSignup`'s primary button element is placed within the DOM
+structure where it is rendered, the dialog DOM structure is appended to the end
+of the DOM tree and it is done by Chakra.
+
+Resources:
+
+- [MDN ARIA: dialog role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role)
+- [W3C ARIA role=dialog](https://www.w3.org/WAI/GL/wiki/Using_ARIA_role%3Ddialog_to_implement_a_modal_dialog_box)
+- [Chakra Modal Accessibility](https://v1.chakra-ui.com/docs/components/overlay/modal#accessibility)
+- [Chakra Drawer Accessibility](https://v1.chakra-ui.com/docs/components/overlay/drawer#accessibility)
+- [DS Button Accessibility](../?path=/docs/components-form-elements-button--docs#accessibility)
+- [DS TextInput Accessibility](../?path=/docs/components-form-elements-textinput--docs#accessibility)
+- [DS Radio Accessibility](../?path=/docs/components-form-elements-radio--docs#accessibility)
+- [DS RadioGroup Accessibility](../?path=/docs/components-form-elements-radiogroup--docs#accessibility)
+
+## Notification Text
+
+The `notificationText` prop can be used to display important or relevant
+information above the form's description text. The text or JSX element passed
+will be rendered in a Reservoir `Notification` component.
+
+## Form Fields
+
+### Comment Field
+
+By default, only the required "Comment" textarea field will render inside the
+`NewsletterSignup` component. There is a 500 character limit for this textarea field.
+
+Validating the "Comment" textarea field is the responsibility of the consuming
+application. If there is an error, the `isInvalidComment` prop can be used to
+toggle the invalid state.
+
+### Category Field
+
+The "Category" field is an optional form field. This radio group form field
+contains three values: "Comment", "Correction", and "Bug". The "Comment" option
+will be selected by default. Use the `showCategoryField` prop to toggle the
+visibility of this field.
+
+### Email Field
+
+The "Email" field is an optional form field. This text input form field is an
+email field. Use the `showEmailField` prop to toggle the visibility of this
+field.
+
+Validating the "Email" address value is the responsibility of the consuming
+application. If there is an error, the `isInvalidEmail` prop can be used to
+toggle the invalid state.
+
+## NewsletterSignup Screens
+
+There are three main screens that will render in the `NewsletterSignup` component:
+the "form", "confirmation", and "error" screens. These are also based on the
+`view` prop available values.
+
+Once the form is submitted, a three (3) second timer starts. Once the timer is
+complete, the "confirmation" screen is rendered. This is the default behavior
+when no success or failure input is provided by the consuming application. This
+means that if there is an error and the consuming application does not tell the
+`NewsletterSignup` component that there is an error, the "confirmation" screen will
+render and this is not the correct or expected behavior.
+
+### Form
+
+This is the initial screen that will render based on the default `view` prop
+value of `"form"`. The "form" screen will render an optional notification,
+an optional description, up to three form fields, the privacy policy link,
+and "Submit" and "Cancel" buttons.
+
+### Confirmation
+
+There are two ways to render the "confirmation" screen. This screen will render
+automatically after the form is submitted and the three (3) second timer is
+complete. The other option is to pass the `view` prop a value of `"confirmation"`
+to render the "confirmation" screen. This is useful if the consuming application
+wants to render the "confirmation" screen immediately after a successful
+API request.
+
+The "confirmation" screen will render a check icon, a basic success message that
+is always the same, an email-specific message when `showEmailField` is set to
+`true`, an additional confirmation message set through the `confirmationText`
+prop, the privacy policy link, and a "Return to Browsing" button. When rendered,
+focus will be set to the confirmation message.
+
+Below is an example of the "confirmation" screen rendered immediately after a
+successful API request. Setting the `view` prop through a `useState` value is
+only one way to update and pass the "confirmation" value to the `NewsletterSignup`
+component. Using the `fetch` API is one possible approach.
+
+
}
+ title="Help and Feedback"
+ onSubmit={onSubmit}
+ />
+ );
+
+ expect(screen.getByTestId("paragraph")).toHaveTextContent(
+ /jsx notification/i
+ );
+ });
+
+ it("renders the `confirmation` screen through the `view` prop", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(/thank you for submitting your feedback/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ /if you provided an email address and require a response/i
+ )
+ ).not.toBeInTheDocument();
+ });
+
+ it("renders the email `confirmation` message when showEmailField is true", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(
+ /if you provided an email address and require a response/i
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("renders the `error` screen through the `view` prop", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(screen.getByText(/oops! something went wrong/i)).toBeInTheDocument();
+ });
+
+ it("submits the form and returns the submitted data", () => {
+ let submittedValues;
+ let onSubmit = (values) => {
+ submittedValues = values;
+ };
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ // The first comment field is the radio button.
+ const commentField = screen.getAllByLabelText(/comment/i)[1];
+ const emailField = screen.getByLabelText(/email/i);
+ const submit = screen.getByRole("button", { name: "Submit" });
+
+ screen.getByText(/bug/i).click();
+ userEvent.type(commentField, "This is a comment");
+ userEvent.type(emailField, "email@email.com");
+
+ submit.click();
+
+ expect(submittedValues).toEqual({
+ category: "bug",
+ comment: "This is a comment",
+ email: "email@email.com",
+ });
+ });
+
+ it("adds hidden fields data to the submitted data", () => {
+ const hiddenFields = {
+ "hidden-field-1": "hidden-field-value-1",
+ "hidden-field-2": "hidden-field-value-2",
+ };
+ let submittedValues;
+ let onSubmit = (values) => {
+ submittedValues = values;
+ };
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ // The first comment field is the radio button.
+ const commentField = screen.getAllByLabelText(/comment/i)[1];
+ const emailField = screen.getByLabelText(/email/i);
+ const submit = screen.getByRole("button", { name: "Submit" });
+
+ screen.getByText(/bug/i).click();
+ userEvent.type(commentField, "This is a comment");
+ userEvent.type(emailField, "email@email.com");
+
+ submit.click();
+
+ expect(submittedValues).toEqual({
+ category: "bug",
+ comment: "This is a comment",
+ email: "email@email.com",
+ "hidden-field-1": "hidden-field-value-1",
+ "hidden-field-2": "hidden-field-value-2",
+ });
+ });
+
+ it("transitions to the `form` screen from the `error` screen", () => {
+ render(
+
+ );
+
+ // Open the dialog.
+ screen.queryByRole("button", { name: "Help and Feedback" }).click();
+
+ const button = screen.queryByRole("button", { name: "Try Again" });
+ expect(
+ screen.queryByText(/oops! something went wrong/i)
+ ).toBeInTheDocument();
+
+ button.click();
+
+ // The `error` screen should no longer display.
+ expect(
+ screen.queryByText(/oops! something went wrong/i)
+ ).not.toBeInTheDocument();
+ expect(button).not.toBeInTheDocument();
+
+ // We are back at the `form` screen.
+ expect(
+ screen.getByRole("textbox", { name: /comment/i })
+ ).toBeInTheDocument();
+ expect(screen.getByText(/500 characters remaining/i)).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ });
+
+ it("renders the UI snapshot correctly", () => {
+ const basic = renderer
+ .create()
+ .toJSON();
+
+ expect(basic).toMatchSnapshot();
+ });
+});
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
new file mode 100644
index 0000000000..d1b11fbf59
--- /dev/null
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -0,0 +1,524 @@
+import {
+ Box,
+ chakra,
+ Drawer,
+ DrawerBody,
+ DrawerContent,
+ DrawerHeader,
+ DrawerOverlay,
+ useColorModeValue,
+ useDisclosure,
+ useMultiStyleConfig,
+ VStack,
+} from "@chakra-ui/react";
+import React, { forwardRef, useEffect, useRef, useState } from "react";
+
+import Button from "../Button/Button";
+import ButtonGroup from "../ButtonGroup/ButtonGroup";
+import Form, { FormField } from "../Form/Form";
+import Icon from "../Icons/Icon";
+import Link from "../Link/Link";
+import Notification from "../Notification/Notification";
+import Radio from "../Radio/Radio";
+import RadioGroup from "../RadioGroup/RadioGroup";
+import Text from "../Text/Text";
+import TextInput from "../TextInput/TextInput";
+import useStateWithDependencies from "../../hooks/useStateWithDependencies";
+import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints";
+import useNewsletterSignupReducer from "./useNewsletterSignupReducer";
+
+export const newsletterSignupViewTypeArray = [
+ "form",
+ "confirmation",
+ "error",
+] as const;
+export type NewsletterSignupViewType =
+ typeof newsletterSignupViewTypeArray[number];
+
+interface NewsletterSignupProps {
+ /** Additional class name to add. */
+ className?: string;
+ /** Used to add additional information to the default confirmation message in
+ * the confirmation view. */
+ confirmationText?: string | JSX.Element;
+ /** Used to add description text above the form input fields in
+ * the initial/form view. */
+ descriptionText?: string | JSX.Element;
+ /** A data object containing key/value pairs that will be added to the form
+ * field submitted data. */
+ hiddenFields?: any;
+ /** ID that other components can cross reference for accessibility purposes */
+ id?: string;
+ /** Toggles the invalid state for the comment field. */
+ isInvalidComment?: boolean;
+ /** Toggles the invalid state for the email field. */
+ isInvalidEmail?: boolean;
+ /** Only used for internal purposes. */
+ isOpen?: boolean;
+ /** Used to add a notification above the description in the
+ * initial/form view.*/
+ notificationText?: string | JSX.Element;
+ /** Only used for internal purposes. */
+ onClose?: any;
+ /** Only used for internal purposes. */
+ onOpen?: any;
+ /** Callback function that will be invoked when the form is submitted.
+ * The returned data object contains key/value pairs including the
+ * values from the `hiddenFields` prop.
+ */
+ onSubmit: (values: { [key: string]: string }) => any;
+ /** Toggles the category radio group field. */
+ showCategoryField?: boolean;
+ /** Toggles the email input field. When set to `true`, an additional
+ * confirmation message will be rendered. */
+ showEmailField?: boolean;
+ /** Used to populate the label on the open button and the `Drawer`'s
+ * header title. */
+ title: string;
+ /** Used to specify what screen should be displayed. */
+ view?: NewsletterSignupViewType;
+}
+
+/**
+ * The `NewsletterSignup` component renders a fixed-positioned button on the bottom
+ * right corner of a page that opens a Chakra `Drawer` popup component. Inside
+ * of the popup, a form is rendered with fields that allows users to provide
+ * feedback. The `NewsletterSignup` component does *not* call any API with the
+ * submitted data; that feature is the responsibility of the consuming
+ * application.
+ */
+export const NewsletterSignup = chakra(
+ forwardRef(
+ (
+ {
+ className,
+ confirmationText,
+ descriptionText,
+ hiddenFields,
+ id = "feedbackbox",
+ isInvalidComment = false,
+ isInvalidEmail = false,
+ notificationText,
+ onSubmit,
+ showCategoryField = false,
+ showEmailField = false,
+ title,
+ view = "form",
+ isOpen,
+ onOpen,
+ onClose,
+ ...rest
+ },
+ ref?
+ ) => {
+ // We want to keep internal state for the view but also
+ // update if the consuming app updates it, based on API
+ // success and failure responses.
+ const [viewType, setViewType] = useStateWithDependencies(view);
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ // Helps keep track of form field state values.
+ const { state, setCategory, setComment, setEmail, clearValues } =
+ useNewsletterSignupReducer();
+ // Hook into NYPL breakpoint
+ const { isLargerThanMobile } = useNYPLBreakpoints();
+ // Chakra's hook to control Drawer's actions.
+ const disclosure = useDisclosure();
+ const finalIsOpen = isOpen ? isOpen : disclosure.isOpen;
+ const finalOnOpen = onOpen ? onOpen : disclosure.onOpen;
+ const finalOnClose = onClose ? onClose : disclosure.onClose;
+ const focusRef = useRef();
+ const styles = useMultiStyleConfig("NewsletterSignup", {});
+ const isFormView = viewType === "form";
+ const isConfirmationView = viewType === "confirmation";
+ const isErrorView = viewType === "error";
+ const confirmationTimeout = 3000;
+ const maxCommentCharacters = 500;
+ const initMinHeight = 165;
+ const initTemplateRows = "auto 1fr";
+ const iconColor = useColorModeValue(null, "dark.ui.typography.body");
+ const minHeightWithCategory = 235;
+ const minHeightWithEmail = 275;
+ const minHeightWithCategoryAndEmail = 345;
+ const notificationHeightAdjustment = 37;
+ const descriptionHeightAdjustment = 24;
+ let drawerMinHeight = initMinHeight;
+ const closeAndResetForm = () => {
+ finalOnClose();
+ setViewType("form");
+ clearValues();
+ };
+ const internalOnSubmit = (e) => {
+ e.preventDefault();
+ let submittedValues = { ...state };
+ if (hiddenFields) {
+ submittedValues = { ...submittedValues, ...hiddenFields };
+ }
+ onSubmit && onSubmit(submittedValues);
+ setIsSubmitted(true);
+ };
+ const notificationElement =
+ isFormView && notificationText ? (
+ div": {
+ py: "xs",
+ },
+ }}
+ width="100%"
+ />
+ ) : undefined;
+ const descriptionElement =
+ isFormView && descriptionText ? (
+
+ {descriptionText}
+
+ ) : undefined;
+ const privacyPolicyField = (
+
+
+ Privacy Policy
+
+
+ );
+
+ // When the submit button is clicked, set a timeout before displaying
+ // the confirmation or error screen. This automatically goes to the
+ // confirmation view after three (3) seconds, but the consuming app
+ // can set the error view if there are any issues.
+ useEffect(() => {
+ let timer;
+ if (isSubmitted) {
+ // If the consuming app does not provide any updates based
+ // on its API response, go to confirmation screen.
+ timer = setTimeout(() => {
+ setIsSubmitted(false);
+ if (isErrorView) {
+ setViewType("error");
+ } else {
+ setViewType("confirmation");
+ }
+ clearValues();
+ }, confirmationTimeout);
+
+ // If the consuming app does pass the API response to the
+ // component, then cancel the timeout above and display the
+ // appropriate screen.
+ if (view !== viewType) {
+ setIsSubmitted(false);
+ setViewType(view);
+ clearTimeout(timer);
+ }
+ }
+
+ return () => clearTimeout(timer);
+ }, [clearValues, isErrorView, isSubmitted, setViewType, view, viewType]);
+
+ // Delay focusing on the confirmation or error message
+ // because it's an element that dynamically gets rendered,
+ // so it is not always available in the DOM.
+ useEffect(() => {
+ let timer;
+ if (viewType === "error" || viewType === "confirmation") {
+ timer = setTimeout(() => {
+ focusRef?.current?.focus();
+ }, 250);
+ }
+ return () => clearTimeout(timer);
+ }, [focusRef, viewType]);
+ if (showCategoryField) {
+ drawerMinHeight = minHeightWithCategory;
+ }
+ if (showEmailField) {
+ drawerMinHeight = minHeightWithEmail;
+ }
+ if (showCategoryField && showEmailField) {
+ drawerMinHeight = minHeightWithCategoryAndEmail;
+ }
+ if (notificationText) {
+ drawerMinHeight += notificationHeightAdjustment;
+ }
+ if (descriptionText) {
+ drawerMinHeight += descriptionHeightAdjustment;
+ }
+ if (notificationText && descriptionText) {
+ drawerMinHeight += 16;
+ }
+ let finalDrawerMinHeight = drawerMinHeight + "px";
+
+ return (
+
+
+
+
+ {/* Adds the opaque background. */}
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ );
+ }
+ )
+);
+
+export function useNewsletterSignup() {
+ const { isOpen, onClose, onOpen } = useDisclosure();
+ const InternalNewsletterSignup = chakra((props) => {
+ return (
+
+ );
+ });
+
+ return {
+ isOpen,
+ onClose,
+ onOpen,
+ NewsletterSignup: InternalNewsletterSignup,
+ };
+}
+
+export default NewsletterSignup;
diff --git a/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
new file mode 100644
index 0000000000..8f05d040d0
--- /dev/null
+++ b/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewsletterSignup renders the UI snapshot correctly 1`] = `
+
+
+
+`;
diff --git a/src/components/NewsletterSignup/useNewsletterSignupReducer.ts b/src/components/NewsletterSignup/useNewsletterSignupReducer.ts
new file mode 100644
index 0000000000..3852cfd1da
--- /dev/null
+++ b/src/components/NewsletterSignup/useNewsletterSignupReducer.ts
@@ -0,0 +1,66 @@
+import { useReducer } from "react";
+
+type Action = {
+ // The fields that we are updating or reset them all at once.
+ type: "category" | "comment" | "email" | "clear";
+ payload?: string;
+};
+interface NewsletterSignupState {
+ category?: string;
+ comment: string;
+ email?: string;
+}
+
+const initialState: NewsletterSignupState = {
+ category: "comment",
+ comment: "",
+ email: "",
+};
+
+/**
+ * Simple reducer to manage the internal state of the form
+ * fields in the NewsletterSignup component.
+ */
+function reducer(state: NewsletterSignupState, action: Action) {
+ switch (action.type) {
+ case "category":
+ return {
+ ...state,
+ category: action.payload,
+ };
+ case "comment":
+ return {
+ ...state,
+ comment: action.payload,
+ };
+ case "email":
+ return {
+ ...state,
+ email: action.payload,
+ };
+ case "clear":
+ default:
+ return initialState;
+ }
+}
+
+/**
+ * DS internal helper reducer hook to manage internal state for the NewsletterSignup
+ * component. Note: this custom hook is not tested directly as it's an
+ * implementation detail of the NewsletterSignup component, following the guidance
+ * of RTL: https://testing-library.com/docs/example-react-hooks-useReducer
+ */
+function useNewsletterSignupReducer() {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const setCategory = (category: string) =>
+ dispatch({ type: "category", payload: category });
+ const setComment = (comment: string) =>
+ dispatch({ type: "comment", payload: comment });
+ const setEmail = (email: string) =>
+ dispatch({ type: "email", payload: email });
+ const clearValues = () => dispatch({ type: "clear" });
+
+ return { state, setCategory, setComment, setEmail, clearValues };
+}
+
+export default useNewsletterSignupReducer;
diff --git a/src/theme/components/newsletterSignup.ts b/src/theme/components/newsletterSignup.ts
new file mode 100644
index 0000000000..1ef5492a31
--- /dev/null
+++ b/src/theme/components/newsletterSignup.ts
@@ -0,0 +1,70 @@
+import { screenreaderOnly } from "./globalMixins";
+
+const FeedbackBox = {
+ parts: [
+ "closeButton",
+ "drawerBody",
+ "drawerContent",
+ "drawerHeader",
+ "openButton",
+ ],
+ baseStyle: {
+ closeButton: {
+ /** This is overriding the default min-height value in order to keep the
+ * button spacing symmetrical. */
+ minHeight: "40px",
+ right: "xs",
+ p: "0",
+ position: "absolute",
+ span: screenreaderOnly(),
+ top: "xs",
+ _dark: {
+ svg: {
+ fill: "dark.ui.typography.heading",
+ },
+ },
+ },
+ drawerBody: {
+ borderLeft: { base: undefined, md: "1px solid" },
+ borderColor: { base: undefined, md: "ui.border.default" },
+ paddingTop: "m",
+ paddingBottom: "m",
+ _dark: {
+ background: "dark.ui.bg.page",
+ borderColor: { base: undefined, md: "dark.ui.border.default" },
+ },
+ },
+ drawerContent: {
+ marginStart: "auto",
+ width: { base: "100%", md: "375px" },
+ },
+ drawerHeader: {
+ alignItems: "baseline",
+ background: "ui.bg.hover",
+ borderBottomWidth: "1px",
+ borderLeftWidth: { base: undefined, md: "1px" },
+ borderTopWidth: "1px",
+ display: "flex",
+ fontSize: "text.default",
+ px: "m",
+ paddingTop: "s",
+ paddingBottom: "s",
+ p: {
+ marginBottom: "0",
+ },
+ _dark: {
+ background: "dark.ui.bg.hover",
+ borderColor: "dark.ui.border.default",
+ },
+ },
+ openButton: {
+ position: "fixed",
+ borderRadius: "0",
+ bottom: "0",
+ right: "0",
+ zIndex: "5",
+ },
+ },
+};
+
+export default FeedbackBox;
diff --git a/src/theme/index.ts b/src/theme/index.ts
index 6602fcb7eb..0076da9b9f 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -35,6 +35,7 @@ import Logo from "./components/logo";
import Modal from "./components/modal";
import MultiSelect from "./components/multiSelect";
import MultiSelectMenuButton from "./components/multiSelectMenuButton";
+import NewsletterSignup from "./components/newsletterSignup";
import NotificationStyles from "./components/notification";
import Pagination from "./components/pagination";
import ProgressIndicator from "./components/progressIndicator";
@@ -118,6 +119,7 @@ const theme: any = {
MultiSelect,
MultiSelectMenuButton,
...NotificationStyles,
+ NewsletterSignup,
Pagination,
ProgressIndicator,
Radio,
From 6e6b37353ff3ee0f146f114c3b7dbaa8699b9288 Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Fri, 8 Sep 2023 16:58:41 -0400
Subject: [PATCH 02/39] NewsletterSignup as exposed Form
---
.../NewsletterSignup/NewsletterSignup.mdx | 298 ------------
.../NewsletterSignup/NewsletterSignup.tsx | 427 ++++++++----------
2 files changed, 194 insertions(+), 531 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx
index 57d290b20f..7c538f73ae 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.mdx
+++ b/src/components/NewsletterSignup/NewsletterSignup.mdx
@@ -85,301 +85,3 @@ Resources:
- [DS Radio Accessibility](../?path=/docs/components-form-elements-radio--docs#accessibility)
- [DS RadioGroup Accessibility](../?path=/docs/components-form-elements-radiogroup--docs#accessibility)
-## Notification Text
-
-The `notificationText` prop can be used to display important or relevant
-information above the form's description text. The text or JSX element passed
-will be rendered in a Reservoir `Notification` component.
-
-## Form Fields
-
-### Comment Field
-
-By default, only the required "Comment" textarea field will render inside the
-`NewsletterSignup` component. There is a 500 character limit for this textarea field.
-
-Validating the "Comment" textarea field is the responsibility of the consuming
-application. If there is an error, the `isInvalidComment` prop can be used to
-toggle the invalid state.
-
-### Category Field
-
-The "Category" field is an optional form field. This radio group form field
-contains three values: "Comment", "Correction", and "Bug". The "Comment" option
-will be selected by default. Use the `showCategoryField` prop to toggle the
-visibility of this field.
-
-### Email Field
-
-The "Email" field is an optional form field. This text input form field is an
-email field. Use the `showEmailField` prop to toggle the visibility of this
-field.
-
-Validating the "Email" address value is the responsibility of the consuming
-application. If there is an error, the `isInvalidEmail` prop can be used to
-toggle the invalid state.
-
-## NewsletterSignup Screens
-
-There are three main screens that will render in the `NewsletterSignup` component:
-the "form", "confirmation", and "error" screens. These are also based on the
-`view` prop available values.
-
-Once the form is submitted, a three (3) second timer starts. Once the timer is
-complete, the "confirmation" screen is rendered. This is the default behavior
-when no success or failure input is provided by the consuming application. This
-means that if there is an error and the consuming application does not tell the
-`NewsletterSignup` component that there is an error, the "confirmation" screen will
-render and this is not the correct or expected behavior.
-
-### Form
-
-This is the initial screen that will render based on the default `view` prop
-value of `"form"`. The "form" screen will render an optional notification,
-an optional description, up to three form fields, the privacy policy link,
-and "Submit" and "Cancel" buttons.
-
-### Confirmation
-
-There are two ways to render the "confirmation" screen. This screen will render
-automatically after the form is submitted and the three (3) second timer is
-complete. The other option is to pass the `view` prop a value of `"confirmation"`
-to render the "confirmation" screen. This is useful if the consuming application
-wants to render the "confirmation" screen immediately after a successful
-API request.
-
-The "confirmation" screen will render a check icon, a basic success message that
-is always the same, an email-specific message when `showEmailField` is set to
-`true`, an additional confirmation message set through the `confirmationText`
-prop, the privacy policy link, and a "Return to Browsing" button. When rendered,
-focus will be set to the confirmation message.
-
-Below is an example of the "confirmation" screen rendered immediately after a
-successful API request. Setting the `view` prop through a `useState` value is
-only one way to update and pass the "confirmation" value to the `NewsletterSignup`
-component. Using the `fetch` API is one possible approach.
-
- {
- fetch(apiEndpoint, {
- method: "POST",
- body: JSON.stringify(values),
- }).then((response) => {
- if (response.ok) {
- // Resolve the promise according to your application.
- // And then call:
- setView("confirmation");
- }
- });
-};
-// ...
-
-`}
- language="jsx"
-/>
-
-### Error
-
-The only way to render the "error" screen is by passing the `view` prop a value
-of `"error"`. This **should** be used if there is an error with the form
-submission API request. This is the responsibility of the consuming application.
-
-The "error" screen will render an error icon, an error message, the privacy
-policy link, and "Try Again" and "Return to Browsing" buttons. When rendered,
-focus will be set to the error message.
-
-Below is an example of the "error" screen rendered immediately after a failed
-API request. Setting the `view` prop through a `useState` value is only one way
-to update and pass the "error" value to the `NewsletterSignup` component. Using the
-`fetch` API is one possible approach.
-
- {
- fetch(apiEndpoint, {
- method: "POST",
- body: JSON.stringify(values),
- })
- .then((response) => {
- if (response.ok) {
- // ...
- }
- })
- .catch((error) => {
- // Reject the promise according to your application.
- // And then call:
- setView("error");
- });
-};
-// ...
-
-`}
- language="jsx"
-/>
-
-## Form Submission Data
-
-Submitted form data can be retrieved when the `NewsletterSignup` component is
-submitted through the required `onSubmit` prop. This prop expects a function and
-it will be called when the form is submitted. Similar to other DS form-components
-that have function props, the data from the component will be returned in the
-function's argument. In this case, it will be a single object.
-
-The submitted form data will be passed as an object that the parent component
-can use. The object will always contain the `comment` field. If the "category"
-field is visible through the `showCategoryField` prop, then the object will also
-contain the `category` field. If the "email" field is visible through the
-`showEmailField` prop, then the object will also contain the `email` field.
-
-Below is an example callback function named `onSubmit` that is passed to the
-`NewsletterSignup` component's `onSubmit` prop. The form data will be returned through
-the function's argument as an object, called `values` in the example below.
-
- {
- console.log("Submitted values:", values);
- // "Submitted values:",
- // {
- // category: "Bug",
- // comment: "Typo in the second paragraph, third sentence.",
- // email: "email@email.com",
- // }
-};
-// ...
-
-`}
- language="jsx"
-/>
-
-### Hidden Field Values
-
-If more key/value pair data needs to be submitted to the API endpoint along with
-the form data from the `NewsletterSignup` component, the `hiddenFields` prop can be
-used. This prop accepts an object of key/value pairs. The object data will be
-merged with the submitted form data.
-
- {
- console.log("Submitted values:", values);
- // {
- // category: "Bug",
- // comment: "Typo in the second paragraph, third sentence.",
- // email: "email@email.com",
- // "hidden-field-1": "hidden-field-value-1",
- // "hidden-field-2": "hidden-field-value-2",
- // }
-};
-// ...
-
-`}
- language="jsx"
-/>
-
-## Programmatically Open
-
-The `NewsletterSignup` component can be opened programmatically if needed, but this
-requires an extra step when importing and implementing the component. Instead
-of importing the `NewsletterSignup` component directly, use the `useNewsletterSignup` hook
-to get the `NewsletterSignup` component and helper functions.
-
-
-
-This hook will return an object with the `NewsletterSignup` component, a boolean
-value, and two functions.
-
-
-
-The `NewsletterSignup` component is the same as the one imported directly, but now
-the `isOpen` value and `onClose` and `onOpen` functions are exposed and
-available to the consuming application. The only function that will be used
-directly is the `onOpen` function. Pass `isOpen` and `onClose` to the
-`NewsletterSignup`.
-
-
-`}
- language="jsx"
-/>
-
-Finally, the `onOpen` function can be used to programmatically open the `NewsletterSignup`
-component through another element or behavior in the consuming app. See the
-example below that uses a custom `Button` to open the Modal. The existing button
-that is rendered by the `NewsletterSignup` component will still work as expected.
-
- {
- const { onOpen, isOpen, onClose, NewsletterSignup } = useNewsletterSignup();
- // ...
- return (
- <>
-
-
- >
- );
-}; `}
- language="jsx"
-/>
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index d1b11fbf59..17a2fc312d 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -1,11 +1,6 @@
import {
Box,
chakra,
- Drawer,
- DrawerBody,
- DrawerContent,
- DrawerHeader,
- DrawerOverlay,
useColorModeValue,
useDisclosure,
useMultiStyleConfig,
@@ -123,8 +118,6 @@ export const NewsletterSignup = chakra(
const { isLargerThanMobile } = useNYPLBreakpoints();
// Chakra's hook to control Drawer's actions.
const disclosure = useDisclosure();
- const finalIsOpen = isOpen ? isOpen : disclosure.isOpen;
- const finalOnOpen = onOpen ? onOpen : disclosure.onOpen;
const finalOnClose = onClose ? onClose : disclosure.onClose;
const focusRef = useRef();
const styles = useMultiStyleConfig("NewsletterSignup", {});
@@ -259,241 +252,209 @@ export const NewsletterSignup = chakra(
return (
-
-
-
- {/* Adds the opaque background. */}
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
+ {confirmationText ? (
+ {confirmationText}
+ ) : undefined}
+
+ {privacyPolicyField}
+
+
+
+
+
+ >
+ )}
+
+ {/* Error Screen */}
+ {isErrorView && (
+ <>
+
+
+
+ Oops! Something went wrong. An error occured while
+ processing your feedback.
+
+
+ {privacyPolicyField}
+
+
+
+
+
+
+ >
+ )}
+
);
}
From d7427a75c33252b184bc242aeccf81b389c4c54a Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Fri, 8 Sep 2023 16:59:48 -0400
Subject: [PATCH 03/39] React EmailSubscription as DS Component
---
.../EmailSubscription/EmailSubscription.mdx | 87 +++++
.../EmailSubscription.stories.tsx | 62 ++++
.../EmailSubscription.test.tsx | 334 ++++++++++++++++++
.../EmailSubscription/EmailSubscription.tsx | 269 ++++++++++++++
.../EmailSubscriptionConfirmation.tsx | 68 ++++
.../EmailSubscriptionForm.tsx | 79 +++++
.../EmailSubscriptionWrapper.tsx | 48 +++
src/components/EmailSubscription/index.ts | 1 +
8 files changed, 948 insertions(+)
create mode 100644 src/components/EmailSubscription/EmailSubscription.mdx
create mode 100644 src/components/EmailSubscription/EmailSubscription.stories.tsx
create mode 100644 src/components/EmailSubscription/EmailSubscription.test.tsx
create mode 100644 src/components/EmailSubscription/EmailSubscription.tsx
create mode 100644 src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx
create mode 100644 src/components/EmailSubscription/EmailSubscriptionForm.tsx
create mode 100644 src/components/EmailSubscription/EmailSubscriptionWrapper.tsx
create mode 100644 src/components/EmailSubscription/index.ts
diff --git a/src/components/EmailSubscription/EmailSubscription.mdx b/src/components/EmailSubscription/EmailSubscription.mdx
new file mode 100644
index 0000000000..74bb80c243
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscription.mdx
@@ -0,0 +1,87 @@
+import { ArgTypes, Canvas, Description, Meta, Source } from "@storybook/blocks";
+
+import * as EmailSubscriptionStories from "./EmailSubscription.stories";
+import Link from "../Link/Link";
+
+
+
+# EmailSubscription
+
+| Component Version | DS Version |
+| ----------------- | ---------- |
+| Added | `1.3.0` |
+| Latest | `1.5.3` |
+
+## Table of Contents
+
+- {Overview}
+- {Component Props}
+- {Accessibility}
+- {Notification Text}
+- {Form Fields}
+- {EmailSubscription Screens}
+- {Form Submission Data}
+- {Programmatically Open}
+
+## Overview
+
+
+
+An NYPL privacy policy link will render within every screen of the EmailSubscription
+("form", "confirmation", and "error" ), and the link will open in a new tab.
+
+**Note**: For the purposes of Storybook, only one (1) `EmailSubscription` component
+example is rendered on the bottom right of this page. The `EmailSubscription` example
+below alternately renders the "confirmation" and "error" screens on each form
+submission. This is just to demonstrate the different states of the component.
+In practice, the consuming app is responsible for handling the form submission.
+
+## Component Props
+
+
+
+[//]: # ()
+
+## Accessibility
+
+The `EmailSubscription` component is a complex component built from various Reservoir
+DS and Chakra components. The two main components are the DS `Button` component
+used to open Chakra's `Drawer` component.
+
+When the primary button is clicked, the dialog opens and focus is set to the
+first focusable element which is the "close" button that contains minus icon in
+the header of the dialog. While opened, focus is trapped within the dialog until
+it is closed either by clicking on the "close" or "Cancel" buttons, pressing the
+"Escape" key, or by clicking outside of the dialog. When the `EmailSubscription`
+component is closed, focus is set back to the primary button that opened the
+dialog.
+
+The markup of the `EmailSubscription` structurally matches the modal dialog pattern
+that is implemented by Chakra's `Modal` and `Drawer` components. The container
+has `role=”dialog”`, `aria-modal=”true”`, `tabindex={0}`, `aria-labelledby` that
+references the title within the dialog, and `aria-describedby` that references a
+descriptive piece of text within the dialog.
+
+Within the `EmailSubscription` component, the radio input field is created from the DS
+`RadioGroup` and `Radio` components, and input fields are created from the DS
+`TextInput` component. Each of these components has their own accessibility
+features documented in their respective Storybook pages.
+
+When the form is submitted, focus is set to the confirmation message or the
+error message if an error occurs.
+
+Whereas the `EmailSubscription`'s primary button element is placed within the DOM
+structure where it is rendered, the dialog DOM structure is appended to the end
+of the DOM tree and it is done by Chakra.
+
+Resources:
+
+- [MDN ARIA: dialog role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role)
+- [W3C ARIA role=dialog](https://www.w3.org/WAI/GL/wiki/Using_ARIA_role%3Ddialog_to_implement_a_modal_dialog_box)
+- [Chakra Modal Accessibility](https://v1.chakra-ui.com/docs/components/overlay/modal#accessibility)
+- [Chakra Drawer Accessibility](https://v1.chakra-ui.com/docs/components/overlay/drawer#accessibility)
+- [DS Button Accessibility](../?path=/docs/components-form-elements-button--docs#accessibility)
+- [DS TextInput Accessibility](../?path=/docs/components-form-elements-textinput--docs#accessibility)
+- [DS Radio Accessibility](../?path=/docs/components-form-elements-radio--docs#accessibility)
+- [DS RadioGroup Accessibility](../?path=/docs/components-form-elements-radiogroup--docs#accessibility)
+
diff --git a/src/components/EmailSubscription/EmailSubscription.stories.tsx b/src/components/EmailSubscription/EmailSubscription.stories.tsx
new file mode 100644
index 0000000000..0af0c88363
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscription.stories.tsx
@@ -0,0 +1,62 @@
+import type { Meta, StoryObj } from "@storybook/react";
+// import { useState } from "react";
+import { withDesign } from "storybook-addon-designs";
+
+import EmailSubscription from "./EmailSubscription"; // emailSubscriptionViewTypeArray,
+// import useStateWithDependencies from "../../hooks/useStateWithDependencies";
+
+const meta: Meta = {
+ title: "Components/Form Elements/EmailSubscription",
+ component: EmailSubscription,
+ decorators: [withDesign],
+};
+
+export default meta;
+type Story = StoryObj;
+
+const EmailSubscriptionWithControls = (args) => {
+ // This hook is used because the `view` prop can be controlled
+ // by Storybook controls.
+ // const [internalView, setInternalView] = useStateWithDependencies(args.view);
+ // const [count, setCount] = useState(0);
+ // // Example hidden field values.
+ // const hiddenFields = {
+ // "hidden-field-1": "hidden-field-value-1",
+ // "hidden-field-2": "hidden-field-value-2",
+ // };
+ // // For the purposes of the Storybook example, the confirmation and
+ // // error screens display on alternate form submissions.
+ // const onSubmit = (values) => {
+ // setCount((prev) => prev + 1);
+ // setInternalView(count % 2 === 0 ? "confirmation" : "error");
+ // console.log("Submitted values:", values);
+ // };
+ return (
+
+ // Call Number: JFE 95-8555
+ //
+ // }
+ // onSubmit={onSubmit}
+ // view={internalView}
+ />
+ );
+};
+
+/**
+ * Main Story for the EmailSubscription component. This must contains the `args`
+ * and `parameters` properties in this object.
+ */
+export const WithControls: Story = {
+ args: {
+ id: "myID",
+ heading: "tester",
+ headingColor: "#BDBDBD",
+ bgColor: "section.research.primary",
+ salesforceSourceCode: "12345",
+ },
+ render: (args) => ,
+};
diff --git a/src/components/EmailSubscription/EmailSubscription.test.tsx b/src/components/EmailSubscription/EmailSubscription.test.tsx
new file mode 100644
index 0000000000..6060991293
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscription.test.tsx
@@ -0,0 +1,334 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { axe } from "jest-axe";
+import * as React from "react";
+import renderer from "react-test-renderer";
+
+import NewsletterSignup from "../NewsletterSignup/NewsletterSignup";
+
+describe("NewsletterSignup Accessibility", () => {
+ it("passes axe accessibility when closed", async () => {
+ const onSubmit = jest.fn();
+ const { container } = render(
+
+ );
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it("passes axe accessibility when opened", async () => {
+ const onSubmit = jest.fn();
+ const { container } = render(
+
+ );
+
+ expect(screen.queryByText(/Comment/i)).not.toBeInTheDocument();
+
+ screen.getByText("Help and Feeback").click();
+ // Just to make sure the dialog is opened.
+ expect(screen.getByText(/Comment/i)).toBeInTheDocument();
+ expect(await axe(container)).toHaveNoViolations();
+ });
+});
+
+describe("NewsletterSignup", () => {
+ let onSubmit = jest.fn();
+
+ it("renders a button component", () => {
+ render();
+
+ expect(
+ screen.getByRole("button", { name: "Help and Feedback" })
+ ).toBeInTheDocument();
+ });
+
+ it("renders the basic content when opened", () => {
+ render();
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(screen.getByTestId("title")).toHaveTextContent("Help and Feedback");
+ expect(
+ screen.getByRole("textbox", { name: /comment/i })
+ ).toBeInTheDocument();
+ expect(screen.getByText(/500 characters remaining/i)).toBeInTheDocument();
+ expect(screen.getByRole("link")).toHaveTextContent("Privacy Policy");
+ // Does not render the radio group or email fields:
+ expect(
+ screen.queryByText(/What is your feedback about/i)
+ ).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(/email/i)).not.toBeInTheDocument();
+
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
+ });
+
+ it("renders optional radio group and email field", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(/what is your feedback about/i)
+ ).toBeInTheDocument();
+ expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
+ });
+
+ it("sets the invalid state for the comment and email field", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(screen.getByText(/please fill out this field/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/please enter a valid email address/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders optional additional description text", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(/Please share your question or feedback/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders optional notification text or JSX", () => {
+ const { rerender } = render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(screen.getByText(/Call Number: JFE 95-8555/i)).toBeInTheDocument();
+
+ rerender(
+ JSX notification}
+ title="Help and Feedback"
+ onSubmit={onSubmit}
+ />
+ );
+
+ expect(screen.getByTestId("paragraph")).toHaveTextContent(
+ /jsx notification/i
+ );
+ });
+
+ it("renders the `confirmation` screen through the `view` prop", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(/thank you for submitting your feedback/i)
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ /if you provided an email address and require a response/i
+ )
+ ).not.toBeInTheDocument();
+ });
+
+ it("renders the email `confirmation` message when showEmailField is true", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(
+ screen.getByText(
+ /if you provided an email address and require a response/i
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("renders the `error` screen through the `view` prop", () => {
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ expect(screen.getByText(/oops! something went wrong/i)).toBeInTheDocument();
+ });
+
+ it("submits the form and returns the submitted data", () => {
+ let submittedValues;
+ let onSubmit = (values) => {
+ submittedValues = values;
+ };
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ // The first comment field is the radio button.
+ const commentField = screen.getAllByLabelText(/comment/i)[1];
+ const emailField = screen.getByLabelText(/email/i);
+ const submit = screen.getByRole("button", { name: "Submit" });
+
+ screen.getByText(/bug/i).click();
+ userEvent.type(commentField, "This is a comment");
+ userEvent.type(emailField, "email@email.com");
+
+ submit.click();
+
+ expect(submittedValues).toEqual({
+ category: "bug",
+ comment: "This is a comment",
+ email: "email@email.com",
+ });
+ });
+
+ it("adds hidden fields data to the submitted data", () => {
+ const hiddenFields = {
+ "hidden-field-1": "hidden-field-value-1",
+ "hidden-field-2": "hidden-field-value-2",
+ };
+ let submittedValues;
+ let onSubmit = (values) => {
+ submittedValues = values;
+ };
+ render(
+
+ );
+
+ const button = screen.getByRole("button", { name: "Help and Feedback" });
+
+ button.click();
+
+ // The first comment field is the radio button.
+ const commentField = screen.getAllByLabelText(/comment/i)[1];
+ const emailField = screen.getByLabelText(/email/i);
+ const submit = screen.getByRole("button", { name: "Submit" });
+
+ screen.getByText(/bug/i).click();
+ userEvent.type(commentField, "This is a comment");
+ userEvent.type(emailField, "email@email.com");
+
+ submit.click();
+
+ expect(submittedValues).toEqual({
+ category: "bug",
+ comment: "This is a comment",
+ email: "email@email.com",
+ "hidden-field-1": "hidden-field-value-1",
+ "hidden-field-2": "hidden-field-value-2",
+ });
+ });
+
+ it("transitions to the `form` screen from the `error` screen", () => {
+ render(
+
+ );
+
+ // Open the dialog.
+ screen.queryByRole("button", { name: "Help and Feedback" }).click();
+
+ const button = screen.queryByRole("button", { name: "Try Again" });
+ expect(
+ screen.queryByText(/oops! something went wrong/i)
+ ).toBeInTheDocument();
+
+ button.click();
+
+ // The `error` screen should no longer display.
+ expect(
+ screen.queryByText(/oops! something went wrong/i)
+ ).not.toBeInTheDocument();
+ expect(button).not.toBeInTheDocument();
+
+ // We are back at the `form` screen.
+ expect(
+ screen.getByRole("textbox", { name: /comment/i })
+ ).toBeInTheDocument();
+ expect(screen.getByText(/500 characters remaining/i)).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ });
+
+ it("renders the UI snapshot correctly", () => {
+ const basic = renderer
+ .create()
+ .toJSON();
+
+ expect(basic).toMatchSnapshot();
+ });
+});
diff --git a/src/components/EmailSubscription/EmailSubscription.tsx b/src/components/EmailSubscription/EmailSubscription.tsx
new file mode 100644
index 0000000000..cbdc6de1e5
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscription.tsx
@@ -0,0 +1,269 @@
+import * as React from "react";
+// import { useRouter } from "next/router"; // next not installed apparently
+import { Spinner } from "@chakra-ui/react";
+import EmailSubscriptionWrapper from "./EmailSubscriptionWrapper";
+import EmailSubscriptionForm from "./EmailSubscriptionForm";
+import EmailSubscriptionConfirmation, {
+ StatusCode,
+} from "./EmailSubscriptionConfirmation";
+import { chakra } from "@chakra-ui/react"; // useStyleConfig removed for now
+import { forwardRef } from "react";
+
+declare global {
+ interface Window {
+ gtag: (...args: any[]) => void;
+ }
+}
+export const emailSubscriptionViewTypeArray = [
+ "form",
+ "confirmation",
+ "error",
+] as const;
+export type EmailSubscriptionViewType =
+ typeof emailSubscriptionViewTypeArray[number];
+
+interface EmailSubscriptionProps {
+ id: string;
+ heading?: string;
+ description?: string;
+ headingColor: string;
+ bgColor?: string;
+ formBaseUrl?: string;
+ formHelperText?: string;
+ formPlaceholder?: string;
+ salesforceListId?: number;
+ salesforceSourceCode: string;
+}
+
+export const EmailSubscription = chakra(
+ forwardRef(
+ ({
+ id,
+ heading,
+ description,
+ headingColor = "ui.white",
+ // @TODO confirm with UX what the default color should be
+ bgColor = "section.research.primary",
+ // @TODO should this even be a prop? I imagine this will be the same for all newsletters?
+ formBaseUrl = "/api/salesforce",
+ formHelperText = "*You will receive email updates from the Library, and you will be able to unsubscribe at any time. To learn more about how the Library uses information you provide, please read our privacy policy.",
+ formPlaceholder,
+ salesforceSourceCode,
+ salesforceListId,
+ ...rest
+ }) =>
+ // ref? // Removed until I figure out what to do with it.
+ {
+ const [input, setInput] = React.useState("");
+ const [isSubmitted, setIsSubmitted] = React.useState(false);
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+
+ const [status, setStatus] = React.useState();
+ // const { asPath } = useRouter();
+
+ const handleSubmit = async (e: any) => {
+ e.preventDefault();
+
+ //Create dynamic Google Analytics values
+ const gaEventCategory = "Email Subscription Forms";
+ // const gaEventActionName = `Subscribe - ${asPath}`; // example: Subscribe - /research
+ const gaEventActionName = `Subscribe - Testing`; // example: Subscribe - /research
+ const gaEventLabel = `Success ${salesforceSourceCode}`; // example: Success research
+
+ setIsSubmitting(true);
+ if (formBaseUrl !== undefined) {
+ // API endpoint where we send form data.
+ const endpoint = `${formBaseUrl}`;
+ // Form the request for sending data to the server.
+ const options = {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ email: e.target.email.value,
+ listID: salesforceListId,
+ sourceCode: salesforceSourceCode,
+ }),
+ };
+
+ // Send the form and await response.
+ try {
+ const response = await fetch(endpoint, options);
+ const result = await response.json();
+
+ window.gtag("event", gaEventActionName, {
+ eventCategory: gaEventCategory,
+ eventLabel: gaEventLabel,
+ });
+
+ setStatus(result.statusCode);
+ setIsSubmitted(true);
+ } catch (error) {
+ setStatus("ERROR");
+ setIsSubmitted(true);
+ /* @TODO maybe it would be usful to submit a
+ GA event with label "Error - ${salesforceSourceCode}"?*/
+ }
+ setIsSubmitting(false);
+ }
+ };
+
+ if (isSubmitting) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {isSubmitted && status ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+ )
+);
+
+// export default function EmailSubscription({
+// id,
+// heading,
+// description,
+// headingColor = "ui.white",
+// // @TODO confirm with UX what the default color should be
+// bgColor = "section.research.primary",
+// // @TODO should this even be a prop? I imagine this will be the same for all newsletters?
+// formBaseUrl = "/api/salesforce",
+// formHelperText = "*You will receive email updates from the Library, and you will be able to unsubscribe at any time. To learn more about how the Library uses information you provide, please read our privacy policy.",
+// formPlaceholder,
+// salesforceSourceCode,
+// salesforceListId,
+// }: EmailSubscriptionProps): JSX.Element {
+// const [input, setInput] = React.useState("");
+// const [isSubmitted, setIsSubmitted] = React.useState(false);
+// const [isSubmitting, setIsSubmitting] = React.useState(false);
+//
+// const [status, setStatus] = React.useState();
+// // const { asPath } = useRouter();
+//
+// const handleSubmit = async (e: any) => {
+// e.preventDefault();
+//
+// //Create dynamic Google Analytics values
+// const gaEventCategory = "Email Subscription Forms";
+// // const gaEventActionName = `Subscribe - ${asPath}`; // example: Subscribe - /research
+// const gaEventActionName = `Subscribe - Testing`; // example: Subscribe - /research
+// const gaEventLabel = `Success ${salesforceSourceCode}`; // example: Success research
+//
+// setIsSubmitting(true);
+// if (formBaseUrl !== undefined) {
+// // API endpoint where we send form data.
+// const endpoint = `${formBaseUrl}`;
+// // Form the request for sending data to the server.
+// const options = {
+// method: "POST",
+// headers: {
+// "Content-Type": "application/json",
+// },
+// body: JSON.stringify({
+// email: e.target.email.value,
+// listID: salesforceListId,
+// sourceCode: salesforceSourceCode,
+// }),
+// };
+//
+// // Send the form and await response.
+// try {
+// const response = await fetch(endpoint, options);
+// const result = await response.json();
+//
+// window.gtag("event", gaEventActionName, {
+// eventCategory: gaEventCategory,
+// eventLabel: gaEventLabel,
+// });
+//
+// setStatus(result.statusCode);
+// setIsSubmitted(true);
+// } catch (error) {
+// setStatus("ERROR");
+// setIsSubmitted(true);
+// /* @TODO maybe it would be usful to submit a
+// GA event with label "Error - ${salesforceSourceCode}"?*/
+// }
+// setIsSubmitting(false);
+// }
+// };
+//
+// if (isSubmitting) {
+// return (
+//
+//
+//
+// );
+// }
+//
+// return (
+//
+// {isSubmitted && status ? (
+//
+// ) : (
+//
+// )}
+//
+// );
+// }
+
+export default EmailSubscription;
diff --git a/src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx b/src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx
new file mode 100644
index 0000000000..0c83558ed1
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx
@@ -0,0 +1,68 @@
+import * as React from "react";
+import { Box } from "@chakra-ui/react";
+import { Icon, IconNames } from "../Icons/Icon";
+
+export type StatusCode = "SUCCESS" | "ERROR" | "TEST_MODE";
+
+const iconTable: Record = {
+ SUCCESS: "check",
+ TEST_MODE: "speakerNotes",
+ ERROR: "errorOutline",
+};
+
+interface EmailSubscriptionConfirmationProps {
+ id: string;
+ status: StatusCode;
+ bgColor?: string;
+ headingColor?: string;
+}
+
+export default function EmailSubscriptionConfirmation({
+ id,
+ bgColor,
+ headingColor,
+ status,
+}: EmailSubscriptionConfirmationProps) {
+ // Manage focus to ensure accessibility when confirmation message is rendered
+ const confirmationMessageRef = React.useRef(null);
+ React.useEffect(() => {
+ confirmationMessageRef.current?.focus();
+ }, []);
+
+ function getStatusMessage(status: StatusCode): string {
+ if (status === "SUCCESS") {
+ return "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.";
+ }
+ if (status === "ERROR") {
+ return "An error has occurred while attempting to save your information. Please refresh this page and try again. If this error persists, contact our e-mail team.";
+ }
+ if (status === "TEST_MODE") {
+ return "Test mode ....";
+ } else {
+ return "UNKNOWN STATUS";
+ }
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/EmailSubscription/EmailSubscriptionForm.tsx b/src/components/EmailSubscription/EmailSubscriptionForm.tsx
new file mode 100644
index 0000000000..d6e492da99
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscriptionForm.tsx
@@ -0,0 +1,79 @@
+import * as React from "react";
+import { Box } from "@chakra-ui/react";
+import { Button } from "../Button/Button";
+import { Form, FormField, FormRow } from "../Form/Form";
+import { Text } from "../Text/Text";
+import { TextInput } from "../TextInput/TextInput";
+
+interface EmailSubscriptionFormProps {
+ id: string;
+ description?: string;
+ onSubmit: (e: React.FormEvent) => void;
+ onChange: (e: string) => void;
+ formInput?: string;
+ formPlaceholder?: string;
+ formHelperText: string;
+}
+
+export default function EmailSubscriptionForm({
+ id,
+ description,
+ onSubmit,
+ onChange,
+ formInput,
+ formPlaceholder,
+ formHelperText,
+}: EmailSubscriptionFormProps): React.ReactElement {
+ return (
+ <>
+ {description}
+
+
+ >
+ );
+}
diff --git a/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx b/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx
new file mode 100644
index 0000000000..c29337534b
--- /dev/null
+++ b/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx
@@ -0,0 +1,48 @@
+import * as React from "react";
+import { Heading } from "../Heading/Heading";
+import { Box } from "@chakra-ui/react";
+
+interface EmailSubscriptionWrapperProps {
+ id: string;
+ bgColor?: string;
+ heading?: string;
+ headingColor?: string;
+ children?: React.ReactElement;
+}
+
+export default function EmailSubscriptionWrapper({
+ id,
+ bgColor,
+ heading,
+ headingColor,
+ children,
+}: EmailSubscriptionWrapperProps): JSX.Element {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/components/EmailSubscription/index.ts b/src/components/EmailSubscription/index.ts
new file mode 100644
index 0000000000..c16aedbbcc
--- /dev/null
+++ b/src/components/EmailSubscription/index.ts
@@ -0,0 +1 @@
+export { default } from "./EmailSubscription";
From 8710debcfd532cfd32935e65e53255be14792bb4 Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Thu, 14 Sep 2023 12:26:55 -0400
Subject: [PATCH 04/39] NewsLetterSignup add getSectionColors function, and
other advances
---
.../NewsletterSignup/NewsletterSignup.mdx | 18 +-
.../NewsletterSignup.stories.tsx | 30 +-
.../NewsletterSignup/NewsletterSignup.tsx | 563 ++++++++----------
src/helpers/getSectionColors.ts | 20 +
src/helpers/types.ts | 100 ++++
5 files changed, 397 insertions(+), 334 deletions(-)
create mode 100644 src/helpers/getSectionColors.ts
diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx
index 7c538f73ae..985d37d8a4 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.mdx
+++ b/src/components/NewsletterSignup/NewsletterSignup.mdx
@@ -7,31 +7,23 @@ import Link from "../Link/Link";
# NewsletterSignup
-| Component Version | DS Version |
-| ----------------- | ---------- |
-| Added | `1.3.0` |
-| Latest | `1.5.3` |
+| Component Version | DS Version |
+| ----------------- | ------------ |
+| Added | `Prerelease` |
+| Latest | `Prerelease` |
## Table of Contents
- {Overview}
- {Component Props}
- {Accessibility}
-- {Notification Text}
-- {Form Fields}
-- {NewsletterSignup Screens}
-- {Form Submission Data}
-- {Programmatically Open}
## Overview
-An NYPL privacy policy link will render within every screen of the NewsletterSignup
-("form", "confirmation", and "error" ), and the link will open in a new tab.
-**Note**: For the purposes of Storybook, only one (1) `NewsletterSignup` component
-example is rendered on the bottom right of this page. The `NewsletterSignup` example
+**Note**: The `NewsletterSignup` example
below alternately renders the "confirmation" and "error" screens on each form
submission. This is just to demonstrate the different states of the component.
In practice, the consuming app is responsible for handling the form submission.
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index 2f69a9718b..14e389c406 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -6,6 +6,7 @@ import NewsletterSignup, {
newsletterSignupViewTypeArray,
} from "./NewsletterSignup";
import useStateWithDependencies from "../../hooks/useStateWithDependencies";
+import { sectionTypeArray } from "../../helpers/types";
const meta: Meta = {
title: "Components/Form Elements/NewsletterSignup",
@@ -18,15 +19,13 @@ const meta: Meta = {
className: { control: false },
hiddenFields: { control: false },
id: { control: false },
- isInvalidComment: { table: { defaultValue: { summary: false } } },
isInvalidEmail: { table: { defaultValue: { summary: false } } },
- isOpen: { table: { disable: true } },
- notificationText: { control: false },
- onClose: { table: { disable: true } },
- onOpen: { table: { disable: true } },
+ newsletterSignupType: {
+ control: { type: "select" },
+ options: sectionTypeArray,
+ table: { defaultValue: { summary: "whatsOn" } },
+ },
onSubmit: { control: false },
- showCategoryField: { table: { defaultValue: { summary: false } } },
- showEmailField: { table: { defaultValue: { summary: true } } },
view: {
control: { type: "select" },
options: newsletterSignupViewTypeArray,
@@ -59,11 +58,6 @@ const NewsletterSignupWithControls = (args) => {
- Call Number: JFE 95-8555
-
- }
onSubmit={onSubmit}
view={internalView}
/>
@@ -78,19 +72,13 @@ export const WithControls: Story = {
args: {
className: undefined,
confirmationText: "",
- descriptionText: "Please share your question or feedback.",
+ descriptionText: undefined,
hiddenFields: undefined,
id: "newsletterSignup-id",
- isInvalidComment: false,
isInvalidEmail: false,
- isOpen: undefined,
- notificationText: undefined,
- onClose: undefined,
- onOpen: undefined,
+ newsletterSignupType: "whatsOn",
onSubmit: undefined,
- showCategoryField: false,
- showEmailField: false,
- title: "Help and Feedback",
+ title: undefined,
view: "form",
},
parameters: {
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index 17a2fc312d..3c36dff69c 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -1,29 +1,29 @@
import {
Box,
chakra,
+ Stack,
useColorModeValue,
- useDisclosure,
useMultiStyleConfig,
VStack,
} from "@chakra-ui/react";
import React, { forwardRef, useEffect, useRef, useState } from "react";
import Button from "../Button/Button";
-import ButtonGroup from "../ButtonGroup/ButtonGroup";
import Form, { FormField } from "../Form/Form";
import Icon from "../Icons/Icon";
import Link from "../Link/Link";
-import Notification from "../Notification/Notification";
-import Radio from "../Radio/Radio";
-import RadioGroup from "../RadioGroup/RadioGroup";
import Text from "../Text/Text";
import TextInput from "../TextInput/TextInput";
+import Heading from "../Heading/Heading";
import useStateWithDependencies from "../../hooks/useStateWithDependencies";
import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints";
import useNewsletterSignupReducer from "./useNewsletterSignupReducer";
+import { getSectionColors } from "../../helpers/getSectionColors";
+import { SectionTypes } from "../../helpers/types";
export const newsletterSignupViewTypeArray = [
"form",
+ "loading",
"confirmation",
"error",
] as const;
@@ -39,34 +39,22 @@ interface NewsletterSignupProps {
/** Used to add description text above the form input fields in
* the initial/form view. */
descriptionText?: string | JSX.Element;
+ /** Optional Used to populate a Text component rendered below the Email field. */
+ formHelper?: string;
/** A data object containing key/value pairs that will be added to the form
* field submitted data. */
hiddenFields?: any;
/** ID that other components can cross reference for accessibility purposes */
id?: string;
- /** Toggles the invalid state for the comment field. */
- isInvalidComment?: boolean;
/** Toggles the invalid state for the email field. */
isInvalidEmail?: boolean;
- /** Only used for internal purposes. */
- isOpen?: boolean;
- /** Used to add a notification above the description in the
- * initial/form view.*/
- notificationText?: string | JSX.Element;
- /** Only used for internal purposes. */
- onClose?: any;
- /** Only used for internal purposes. */
- onOpen?: any;
+ /* Optional value to determine the section color highlight */
+ newsletterSignupType?: SectionTypes;
/** Callback function that will be invoked when the form is submitted.
* The returned data object contains key/value pairs including the
* values from the `hiddenFields` prop.
*/
onSubmit: (values: { [key: string]: string }) => any;
- /** Toggles the category radio group field. */
- showCategoryField?: boolean;
- /** Toggles the email input field. When set to `true`, an additional
- * confirmation message will be rendered. */
- showEmailField?: boolean;
/** Used to populate the label on the open button and the `Drawer`'s
* header title. */
title: string;
@@ -75,33 +63,26 @@ interface NewsletterSignupProps {
}
/**
- * The `NewsletterSignup` component renders a fixed-positioned button on the bottom
- * right corner of a page that opens a Chakra `Drawer` popup component. Inside
- * of the popup, a form is rendered with fields that allows users to provide
- * feedback. The `NewsletterSignup` component does *not* call any API with the
- * submitted data; that feature is the responsibility of the consuming
- * application.
+ * The NewsletterSignup component provides a way for patrons to register for an
+ * email-based newsletter distribution list.
+ *
+ * The component can show four different views, depending on the state of the
+ * email submission: form, loading, confirmation, and error.
*/
export const NewsletterSignup = chakra(
forwardRef(
(
{
className,
- confirmationText,
- descriptionText,
+ confirmationText = "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
+ descriptionText = "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
hiddenFields,
- id = "feedbackbox",
- isInvalidComment = false,
+ id,
isInvalidEmail = false,
- notificationText,
+ newsletterSignupType = "whatsOn",
onSubmit,
- showCategoryField = false,
- showEmailField = false,
- title,
+ title = "Sign Up for Our Newsletter!",
view = "form",
- isOpen,
- onOpen,
- onClose,
...rest
},
ref?
@@ -112,34 +93,26 @@ export const NewsletterSignup = chakra(
const [viewType, setViewType] = useStateWithDependencies(view);
const [isSubmitted, setIsSubmitted] = useState(false);
// Helps keep track of form field state values.
- const { state, setCategory, setComment, setEmail, clearValues } =
- useNewsletterSignupReducer();
+ const { state, setEmail, clearValues } = useNewsletterSignupReducer();
// Hook into NYPL breakpoint
const { isLargerThanMobile } = useNYPLBreakpoints();
- // Chakra's hook to control Drawer's actions.
- const disclosure = useDisclosure();
- const finalOnClose = onClose ? onClose : disclosure.onClose;
+
const focusRef = useRef();
const styles = useMultiStyleConfig("NewsletterSignup", {});
const isFormView = viewType === "form";
const isConfirmationView = viewType === "confirmation";
const isErrorView = viewType === "error";
const confirmationTimeout = 3000;
- const maxCommentCharacters = 500;
- const initMinHeight = 165;
+
const initTemplateRows = "auto 1fr";
const iconColor = useColorModeValue(null, "dark.ui.typography.body");
- const minHeightWithCategory = 235;
- const minHeightWithEmail = 275;
- const minHeightWithCategoryAndEmail = 345;
- const notificationHeightAdjustment = 37;
- const descriptionHeightAdjustment = 24;
- let drawerMinHeight = initMinHeight;
- const closeAndResetForm = () => {
- finalOnClose();
- setViewType("form");
- clearValues();
- };
+
+ console.log(getSectionColors(newsletterSignupType));
+ // Unused since cancel button removed. Maybe useful later?
+ // const closeAndResetForm = () => {
+ // setViewType("form");
+ // clearValues();
+ // };
const internalOnSubmit = (e) => {
e.preventDefault();
let submittedValues = { ...state };
@@ -149,41 +122,21 @@ export const NewsletterSignup = chakra(
onSubmit && onSubmit(submittedValues);
setIsSubmitted(true);
};
- const notificationElement =
- isFormView && notificationText ? (
- div": {
- py: "xs",
- },
- }}
- width="100%"
- />
- ) : undefined;
+
const descriptionElement =
isFormView && descriptionText ? (
-
- {descriptionText}
-
+ {descriptionText}
) : undefined;
- const privacyPolicyField = (
-
-
- Privacy Policy
-
-
+
+ const privacyPolicy = (
+
+ Privacy Policy
+
);
// When the submit button is clicked, set a timeout before displaying
@@ -230,108 +183,52 @@ export const NewsletterSignup = chakra(
}
return () => clearTimeout(timer);
}, [focusRef, viewType]);
- if (showCategoryField) {
- drawerMinHeight = minHeightWithCategory;
- }
- if (showEmailField) {
- drawerMinHeight = minHeightWithEmail;
- }
- if (showCategoryField && showEmailField) {
- drawerMinHeight = minHeightWithCategoryAndEmail;
- }
- if (notificationText) {
- drawerMinHeight += notificationHeightAdjustment;
- }
- if (descriptionText) {
- drawerMinHeight += descriptionHeightAdjustment;
- }
- if (notificationText && descriptionText) {
- drawerMinHeight += 16;
- }
- let finalDrawerMinHeight = drawerMinHeight + "px";
return (
-
-
+
-
-
- >
- )}
-
-
+
+ >
+ )}
+
+
+
+ //
+ //
-
- >
- );
-}
diff --git a/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx b/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx
deleted file mode 100644
index c29337534b..0000000000
--- a/src/components/EmailSubscription/EmailSubscriptionWrapper.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as React from "react";
-import { Heading } from "../Heading/Heading";
-import { Box } from "@chakra-ui/react";
-
-interface EmailSubscriptionWrapperProps {
- id: string;
- bgColor?: string;
- heading?: string;
- headingColor?: string;
- children?: React.ReactElement;
-}
-
-export default function EmailSubscriptionWrapper({
- id,
- bgColor,
- heading,
- headingColor,
- children,
-}: EmailSubscriptionWrapperProps): JSX.Element {
- return (
-
-
-
- {children}
-
-
- );
-}
diff --git a/src/components/EmailSubscription/index.ts b/src/components/EmailSubscription/index.ts
deleted file mode 100644
index c16aedbbcc..0000000000
--- a/src/components/EmailSubscription/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./EmailSubscription";
diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx
index 985d37d8a4..9c0b43b13d 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.mdx
+++ b/src/components/NewsletterSignup/NewsletterSignup.mdx
@@ -34,6 +34,53 @@ In practice, the consuming app is responsible for handling the form submission.
+ {
+ e.preventDefault();
+
+ //Create dynamic Google Analytics values
+ const gaEventCategory = "Email Subscription Forms";
+ // const gaEventActionName = `Subscribe - ${asPath}`; // example: Subscribe - /research
+ const gaEventActionName = `Subscribe - Testing`; // example: Subscribe - /research
+ const gaEventLabel = `Success ${salesforceSourceCode}`; // example: Success research
+
+ setIsSubmitting(true);
+
+ const endpoint = "/api/salesforce";
+
+ // Form the request for sending data to the server.
+ const options = {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ email: e.target.email.value,
+ listID: salesforceListId,
+ sourceCode: salesforceSourceCode,
+ }),
+
+
+ // Send the form and await response.
+ try {
+ const response = await fetch(endpoint, options);
+ const result = await response.json();
+
+ window.gtag("event", gaEventActionName, {
+ eventCategory: gaEventCategory,
+ eventLabel: gaEventLabel,
+ });
+
+ setStatus(result.statusCode);
+ setIsSubmitted(true);
+ } catch (error) {
+ setStatus("ERROR");
+ setIsSubmitted(true);
+ }
+ setIsSubmitting(false);
+ }
+ };' />
+
## Accessibility
The `NewsletterSignup` component is a complex component built from various Reservoir
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index 14e389c406..02db7e133b 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -5,7 +5,6 @@ import { withDesign } from "storybook-addon-designs";
import NewsletterSignup, {
newsletterSignupViewTypeArray,
} from "./NewsletterSignup";
-import useStateWithDependencies from "../../hooks/useStateWithDependencies";
import { sectionTypeArray } from "../../helpers/types";
const meta: Meta = {
@@ -25,7 +24,6 @@ const meta: Meta = {
options: sectionTypeArray,
table: { defaultValue: { summary: "whatsOn" } },
},
- onSubmit: { control: false },
view: {
control: { type: "select" },
options: newsletterSignupViewTypeArray,
@@ -38,30 +36,26 @@ export default meta;
type Story = StoryObj;
const NewsletterSignupWithControls = (args) => {
- // This hook is used because the `view` prop can be controlled
- // by Storybook controls.
- const [internalView, setInternalView] = useStateWithDependencies(args.view);
- const [count, setCount] = useState(0);
- // Example hidden field values.
- const hiddenFields = {
- "hidden-field-1": "hidden-field-value-1",
- "hidden-field-2": "hidden-field-value-2",
+ const [view, setView] = useState("form");
+ const [isInvalidEmail, setIsInvalidEmail] = useState(false);
+ const onSubmit = (values?: { [key: string]: string }) => {
+ switch (values.email) {
+ case "":
+ setIsInvalidEmail(true);
+ break;
+ case "error@nypl.org":
+ setView("error");
+ break;
+ }
+ console.log("Submitted values:", values, isInvalidEmail, view);
};
- // For the purposes of the Storybook example, the confirmation and
- // error screens display on alternate form submissions.
- const onSubmit = (values) => {
- setCount((prev) => prev + 1);
- setInternalView(count % 2 === 0 ? "confirmation" : "error");
- console.log("Submitted values:", values);
- };
- return (
-
- );
+ return ;
+};
+
+// Example hidden field values.
+const hiddenFields = {
+ "hidden-field-1": "hidden-field-value-1",
+ "hidden-field-2": "hidden-field-value-2",
};
/**
@@ -71,9 +65,10 @@ const NewsletterSignupWithControls = (args) => {
export const WithControls: Story = {
args: {
className: undefined,
- confirmationText: "",
+ confirmationText:
+ "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
descriptionText: undefined,
- hiddenFields: undefined,
+ hiddenFields: { hiddenFields },
id: "newsletterSignup-id",
isInvalidEmail: false,
newsletterSignupType: "whatsOn",
@@ -84,7 +79,7 @@ export const WithControls: Story = {
parameters: {
design: {
type: "figma",
- url: "",
+ url: "https://www.figma.com/file/qShodlfNCJHb8n03IFyApM/Main?node-id=80849%3A174194&mode=dev",
},
jest: "NewsletterSignup.test.tsx",
},
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index 92f5f7b86b..52c5a32c9f 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -6,7 +6,7 @@ import {
useStyleConfig,
VStack,
} from "@chakra-ui/react";
-import React, { forwardRef, useEffect, useRef, useState } from "react";
+import React, { forwardRef, useRef, useState } from "react";
import Button from "../Button/Button";
import Form, { FormField } from "../Form/Form";
@@ -15,9 +15,8 @@ import Link from "../Link/Link";
import Text from "../Text/Text";
import TextInput from "../TextInput/TextInput";
import Heading from "../Heading/Heading";
-import useStateWithDependencies from "../../hooks/useStateWithDependencies";
+//import useStateWithDependencies from "../../hooks/useStateWithDependencies";
import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints";
-import useNewsletterSignupReducer from "./useNewsletterSignupReducer";
import { getSectionColors } from "../../helpers/getSectionColors";
import { SectionTypes } from "../../helpers/types";
@@ -36,7 +35,7 @@ interface NewsletterSignupProps {
* the confirmation view. */
confirmationText?: string | JSX.Element;
/** Used to add description text above the form input fields in
- * the initial/form view. */
+ * the initial/form view. Accepts a string or a JSX */
descriptionText?: string | JSX.Element;
/** Optional: Used to populate the Text component rendered below the input field. */
formHelper?: string;
@@ -49,14 +48,14 @@ interface NewsletterSignupProps {
isInvalidEmail?: boolean;
/* Optional value to determine the section color highlight */
newsletterSignupType?: SectionTypes;
- /** Callback function that will be invoked when the form is submitted.
- * The returned data object contains key/value pairs including the
- * values from the `hiddenFields` prop.
+ /** A callback handler function that will be called when the form is submitted.
+ * The function provided must expect an object of key:value pairs. This object will
+ * include {email}, being the user's input, plus any objects passed via the hiddenValues prop.
*/
onSubmit: (values: { [key: string]: string }) => any;
- /* Used to populate the
header title. */
+ /** Used to populate the
header title. */
title: string;
- /** Used to specify what screen should be displayed. */
+ /** Used to specify what is displayed in the component content area. */
view?: NewsletterSignupViewType;
}
@@ -88,37 +87,71 @@ export const NewsletterSignup = chakra(
// We want to keep internal state for the view but also
// update if the consuming app updates it, based on API
// success and failure responses.
- const [viewType, setViewType] = useStateWithDependencies(view);
- const [isSubmitted, setIsSubmitted] = useState(false);
- // Helps keep track of form field state values.
- const { state, setEmail, clearValues } = useNewsletterSignupReducer();
+ // const [viewType, setViewType] = useStateWithDependencies(view);
+ // const [badEmail, setBadEmail] = useStateWithDependencies(isInvalidEmail);
+ const [buttonClicked, setButtonClicked] = useState(false);
+ const [email, setEmail] = useState("");
+
// Hook into NYPL breakpoint
const { isLargerThanMobile } = useNYPLBreakpoints();
-
- const focusRef = useRef();
+ console.log("view: ", view);
+ console.log("signup type: ", newsletterSignupType);
+ const focusRef = useRef(); // @todo Is this needed? It is a holdover from FeedbackBox
const styles = useStyleConfig("NewsletterSignup", {});
- const isFormView = viewType === "form";
- const isConfirmationView = viewType === "confirmation";
- const isErrorView = viewType === "error";
- const confirmationTimeout = 3000;
+ // This makes it slightly simpler to test for which
+ const formView = view === "form";
+ const confirmationView = view === "confirmation";
+ const errorView = view === "error";
+
const iconColor = useColorModeValue(null, "dark.ui.typography.body");
- let buttonClicked = false;
+ // let buttonClicked = false;
// Unused since cancel button removed. Maybe useful later?
// const closeAndResetForm = () => {
// setViewType("form");
// clearValues();
// };
+
+ // Where is this "e" coming from? Is it just magically passed?
const internalOnSubmit = (e) => {
e.preventDefault();
- let submittedValues = { ...state };
- if (hiddenFields) {
- submittedValues = { ...submittedValues, ...hiddenFields };
+ setButtonClicked(true);
+ const submittedValues = { email, ...hiddenFields };
+ onSubmit(submittedValues); // onSubmit comes from the consuming app
+
+ // If the consuming app does send either a new view or an isInvalidEmail, wait three seconds for either of
+ // those values, then just give the confirmation screen.
+ // @todo This currently does not work.
+ if (view === "form" && !isInvalidEmail) {
+ // Wait three seconds. If no return from the onSubmit function, assume the best and show the confirmation screen.
+ let timer = setTimeout(() => {
+ view = "confirmation";
+ }, 3000);
+ // console.log(
+ // "viewType:",
+ // viewType,
+ // "view",
+ // view,
+ // "badEmail:",
+ // badEmail,
+ // "isInvalidEmail:",
+ // isInvalidEmail
+ // );
+ // If the consuming app provides an update sooner than three seconds, cancel the timer.
+
+ // @todo Maybe this is the key?
+ // if (view !== viewType || isInvalidEmail !== badEmail) {
+ // setViewType(view); // Note: in the case of isInvalidEmail, this will still be a form
+ // setBadEmail(isInvalidEmail);
+ // setButtonClicked(false); // Only relevant if email address is bad.
+ // console.log("Kill timer", Date.now(), timer);
+ // clearTimeout(timer);
+ // }
+ console.log("Dead timer", Date.now(), timer);
+ return "";
}
- onSubmit && onSubmit(submittedValues);
- buttonClicked = true;
- };
+ }; // Close internalOnSubmit
const privacyPolicy = (
);
- // When the submit button is clicked, set a timeout before displaying
- // the confirmation or error screen. This automatically goes to the
- // confirmation view after three (3) seconds, but the consuming app
- // can set the error view if there are any issues.
- useEffect(() => {
- console.log(buttonClicked);
- // START Move this chunk directly into onSubmit function and forget this whole useEffect thing.
- let timer;
- if (buttonClicked) {
- // If the consuming app does not provide any updates based
- // on its API response, go to confirmation screen.
- timer = setTimeout(() => {
- setIsSubmitted(false);
- if (isErrorView) {
- setViewType("error");
- } else {
- setViewType("confirmation");
- }
- clearValues();
- }, confirmationTimeout);
-
- // If the consuming app does pass the API response to the
- // component, then cancel the timeout above and display the
- // appropriate screen.
- if (view !== viewType) {
- setIsSubmitted(false);
- setViewType(view);
- clearTimeout(timer);
- }
- }
-
- return () => clearTimeout(timer); // END
- }, [
- clearValues,
- isErrorView,
- isSubmitted,
- setViewType,
- view,
- viewType,
- buttonClicked,
- ]);
-
- // Do we need to focus on the [whatever]?
-
- // Delay focusing on the confirmation or error message
- // because it's an element that dynamically gets rendered,
- // so it is not always available in the DOM.
- useEffect(() => {
- let timer;
- if (viewType === "error" || viewType === "confirmation") {
- timer = setTimeout(() => {
- focusRef?.current?.focus();
- }, 250);
- }
- return () => clearTimeout(timer);
- }, [focusRef, viewType]);
-
return (
{/* Initial form Screen */}
- {isFormView && (
+ {console.log("formView?", formView)}
+ {formView && (
<>
+ {/**/}
setEmail(e.target.value)}
+ onChange={(e) => setEmail(e.target.value)} // e.target.value is what the user has input. So when they hit "submit" it will be stored in whatever variable we wish in the setEmail function.
placeholder="Enter your email address here"
type="email"
- value={state.email}
+ value={email}
/>
@@ -245,7 +223,7 @@ export const NewsletterSignup = chakra(
)}
{/* Confirmation Screen */}
- {isConfirmationView && (
+ {confirmationView && (
<>
-
- Thank you for submitting your feedback.
-
- {confirmationText ? (
- {confirmationText}
- ) : undefined}
+ {confirmationText}
>
)}
{/* Error Screen */}
- {isErrorView && (
+ {errorView && (
<>
{/* End action div */}
- //
);
}
)
diff --git a/src/components/NewsletterSignup/useNewsletterSignupReducer.ts b/src/components/NewsletterSignup/useNewsletterSignupReducer.ts
deleted file mode 100644
index 3852cfd1da..0000000000
--- a/src/components/NewsletterSignup/useNewsletterSignupReducer.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useReducer } from "react";
-
-type Action = {
- // The fields that we are updating or reset them all at once.
- type: "category" | "comment" | "email" | "clear";
- payload?: string;
-};
-interface NewsletterSignupState {
- category?: string;
- comment: string;
- email?: string;
-}
-
-const initialState: NewsletterSignupState = {
- category: "comment",
- comment: "",
- email: "",
-};
-
-/**
- * Simple reducer to manage the internal state of the form
- * fields in the NewsletterSignup component.
- */
-function reducer(state: NewsletterSignupState, action: Action) {
- switch (action.type) {
- case "category":
- return {
- ...state,
- category: action.payload,
- };
- case "comment":
- return {
- ...state,
- comment: action.payload,
- };
- case "email":
- return {
- ...state,
- email: action.payload,
- };
- case "clear":
- default:
- return initialState;
- }
-}
-
-/**
- * DS internal helper reducer hook to manage internal state for the NewsletterSignup
- * component. Note: this custom hook is not tested directly as it's an
- * implementation detail of the NewsletterSignup component, following the guidance
- * of RTL: https://testing-library.com/docs/example-react-hooks-useReducer
- */
-function useNewsletterSignupReducer() {
- const [state, dispatch] = useReducer(reducer, initialState);
- const setCategory = (category: string) =>
- dispatch({ type: "category", payload: category });
- const setComment = (comment: string) =>
- dispatch({ type: "comment", payload: comment });
- const setEmail = (email: string) =>
- dispatch({ type: "email", payload: email });
- const clearValues = () => dispatch({ type: "clear" });
-
- return { state, setCategory, setComment, setEmail, clearValues };
-}
-
-export default useNewsletterSignupReducer;
diff --git a/src/hooks/useStateWithDependencies.ts b/src/hooks/useStateWithDependencies.ts
index 2130f30414..ca8df46591 100644
--- a/src/hooks/useStateWithDependencies.ts
+++ b/src/hooks/useStateWithDependencies.ts
@@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
/**
* DS internal helper hook to use state with prop dependencies.
+ *
+ * This hook should NOT be used by consuming apps. DS components should use this hook rather than useState() whenever
+ * a change in state is expected for a given prop from a consuming app. The hook updates the DS component with useEffect().
*/
function useStateWithDependencies(initialValue: any): typeof initialValue {
const [value, setValue] = useState(initialValue);
From 706bf00e25c34d2bd5630255957f0b56ab76d634 Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Fri, 22 Sep 2023 18:09:47 -0400
Subject: [PATCH 07/39] NewsletterSignup Near Complete Functionality
---
.../NewsletterSignup.stories.tsx | 12 ++-
.../NewsletterSignup/NewsletterSignup.tsx | 98 +++++++++----------
2 files changed, 57 insertions(+), 53 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index 02db7e133b..62c9918c11 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -46,10 +46,20 @@ const NewsletterSignupWithControls = (args) => {
case "error@nypl.org":
setView("error");
break;
+ case "confirmation@nypl.org":
+ setView("confirmation");
+ break;
}
console.log("Submitted values:", values, isInvalidEmail, view);
};
- return ;
+ return (
+
+ );
};
// Example hidden field values.
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index 52c5a32c9f..a83b7e03be 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -6,7 +6,7 @@ import {
useStyleConfig,
VStack,
} from "@chakra-ui/react";
-import React, { forwardRef, useRef, useState } from "react";
+import React, { forwardRef, useEffect, useRef, useState } from "react";
import Button from "../Button/Button";
import Form, { FormField } from "../Form/Form";
@@ -19,6 +19,7 @@ import Heading from "../Heading/Heading";
import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints";
import { getSectionColors } from "../../helpers/getSectionColors";
import { SectionTypes } from "../../helpers/types";
+import useStateWithDependencies from "../../hooks/useStateWithDependencies";
export const newsletterSignupViewTypeArray = [
"form",
@@ -87,70 +88,26 @@ export const NewsletterSignup = chakra(
// We want to keep internal state for the view but also
// update if the consuming app updates it, based on API
// success and failure responses.
- // const [viewType, setViewType] = useStateWithDependencies(view);
- // const [badEmail, setBadEmail] = useStateWithDependencies(isInvalidEmail);
+ const [viewType, setViewType] = useStateWithDependencies(view);
+ const [badEmail, setBadEmail] = useStateWithDependencies(isInvalidEmail);
const [buttonClicked, setButtonClicked] = useState(false);
const [email, setEmail] = useState("");
// Hook into NYPL breakpoint
const { isLargerThanMobile } = useNYPLBreakpoints();
- console.log("view: ", view);
- console.log("signup type: ", newsletterSignupType);
const focusRef = useRef(); // @todo Is this needed? It is a holdover from FeedbackBox
const styles = useStyleConfig("NewsletterSignup", {});
- // This makes it slightly simpler to test for which
- const formView = view === "form";
- const confirmationView = view === "confirmation";
- const errorView = view === "error";
-
+ // The viewType may change as a result of something happening in the internalOnSubmit function.
+ const formView = viewType === "form";
+ const confirmationView = viewType === "confirmation";
+ const errorView = viewType === "error";
const iconColor = useColorModeValue(null, "dark.ui.typography.body");
- // let buttonClicked = false;
-
- // Unused since cancel button removed. Maybe useful later?
- // const closeAndResetForm = () => {
- // setViewType("form");
- // clearValues();
- // };
-
- // Where is this "e" coming from? Is it just magically passed?
const internalOnSubmit = (e) => {
e.preventDefault();
setButtonClicked(true);
const submittedValues = { email, ...hiddenFields };
onSubmit(submittedValues); // onSubmit comes from the consuming app
-
- // If the consuming app does send either a new view or an isInvalidEmail, wait three seconds for either of
- // those values, then just give the confirmation screen.
- // @todo This currently does not work.
- if (view === "form" && !isInvalidEmail) {
- // Wait three seconds. If no return from the onSubmit function, assume the best and show the confirmation screen.
- let timer = setTimeout(() => {
- view = "confirmation";
- }, 3000);
- // console.log(
- // "viewType:",
- // viewType,
- // "view",
- // view,
- // "badEmail:",
- // badEmail,
- // "isInvalidEmail:",
- // isInvalidEmail
- // );
- // If the consuming app provides an update sooner than three seconds, cancel the timer.
-
- // @todo Maybe this is the key?
- // if (view !== viewType || isInvalidEmail !== badEmail) {
- // setViewType(view); // Note: in the case of isInvalidEmail, this will still be a form
- // setBadEmail(isInvalidEmail);
- // setButtonClicked(false); // Only relevant if email address is bad.
- // console.log("Kill timer", Date.now(), timer);
- // clearTimeout(timer);
- // }
- console.log("Dead timer", Date.now(), timer);
- return "";
- }
}; // Close internalOnSubmit
const privacyPolicy = (
@@ -163,6 +120,44 @@ export const NewsletterSignup = chakra(
);
+ // When the submit button is clicked, set a timeout before displaying
+ // the confirmation or error screen. This automatically goes to the
+ // confirmation view after three (3) seconds, but the consuming app
+ // can set the error view if there are any issues.
+ // @todo Is useEffect bad? I read that is not good. But I can't put any of this logic in the internalOnSubmit function
+ // because it won't work and I am not knowledgeable enough to figure out an alternate solution. So this is adapted from FeebackBox.
+ useEffect(() => {
+ console.log("useEffect");
+ let timer;
+ if (buttonClicked) {
+ // If the consuming app does not provide any updates based
+ // on its API response, go to confirmation screen.
+ timer = setTimeout(() => {
+ setViewType("confirmation");
+ }, 3000);
+ // If the consuming app does pass the API response to the
+ // component, then cancel the timeout above and display the
+ // appropriate screen.
+ if (view !== viewType || isInvalidEmail !== badEmail) {
+ setBadEmail(false); // @todo It won't let you resubmit.
+ setButtonClicked(false);
+ setViewType(view);
+ clearTimeout(timer);
+ }
+ }
+
+ return () => clearTimeout(timer);
+ }, [
+ buttonClicked,
+ badEmail,
+ errorView,
+ isInvalidEmail,
+ setBadEmail,
+ setViewType,
+ view,
+ viewType,
+ ]);
+
return (
{/* Initial form Screen */}
- {console.log("formView?", formView)}
{formView && (
<>
From e7952c1770cc9396319c36fb1f44ade9c96fc41c Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Mon, 25 Sep 2023 18:04:35 -0400
Subject: [PATCH 08/39] NewsletterSignup Updates & Fixes
---
.../NewsletterSignup.stories.tsx | 79 +++++++++++++------
.../NewsletterSignup/NewsletterSignup.tsx | 21 ++---
src/theme/components/newsletterSignup.ts | 9 ++-
3 files changed, 68 insertions(+), 41 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index 62c9918c11..d97b683ca0 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -1,11 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
-import { useState } from "react";
import { withDesign } from "storybook-addon-designs";
-
-import NewsletterSignup, {
- newsletterSignupViewTypeArray,
-} from "./NewsletterSignup";
-import { sectionTypeArray } from "../../helpers/types";
+import useStateWithDependencies from "../../hooks/useStateWithDependencies";
+import NewsletterSignup from "./NewsletterSignup";
const meta: Meta = {
title: "Components/Form Elements/NewsletterSignup",
@@ -16,18 +12,47 @@ const meta: Meta = {
},
argTypes: {
className: { control: false },
- hiddenFields: { control: false },
id: { control: false },
- isInvalidEmail: { table: { defaultValue: { summary: false } } },
+ confirmationText: {
+ control: "text",
+ table: {
+ defaultValue: {
+ summary:
+ "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
+ },
+ },
+ },
+ descriptionText: {
+ control: "text",
+ table: {
+ defaultValue: {
+ summary:
+ "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
+ },
+ },
+ },
+ formHelperText: {
+ control: "text",
+ },
newsletterSignupType: {
- control: { type: "select" },
- options: sectionTypeArray,
- table: { defaultValue: { summary: "whatsOn" } },
+ control: "select",
+ table: {
+ defaultValue: {
+ summary: "whatsOn",
+ },
+ },
},
view: {
- control: { type: "select" },
- options: newsletterSignupViewTypeArray,
- table: { defaultValue: { summary: "form" } },
+ control: "select",
+ table: {
+ defaultValue: {
+ summary: "form",
+ },
+ },
+ },
+ title: {
+ control: "text",
+ table: { defaultValue: { summary: "Sign Up for Our Newsletter!" } },
},
},
};
@@ -36,8 +61,13 @@ export default meta;
type Story = StoryObj;
const NewsletterSignupWithControls = (args) => {
- const [view, setView] = useState("form");
- const [isInvalidEmail, setIsInvalidEmail] = useState(false);
+ const [view, setView] = useStateWithDependencies(args.view);
+ const [isInvalidEmail, setIsInvalidEmail] = useStateWithDependencies(
+ args.isInvalidEmail
+ );
+ const [confirmationText, setConfirmationText] = useStateWithDependencies(
+ args.confirmationText
+ );
const onSubmit = (values?: { [key: string]: string }) => {
switch (values.email) {
case "":
@@ -48,16 +78,20 @@ const NewsletterSignupWithControls = (args) => {
break;
case "confirmation@nypl.org":
setView("confirmation");
+ setConfirmationText(
+ "This is going to change your life. Check out those values in the console!"
+ );
break;
}
- console.log("Submitted values:", values, isInvalidEmail, view);
+ console.log("Submitted values:", values, isInvalidEmail);
};
return (
);
};
@@ -76,14 +110,15 @@ export const WithControls: Story = {
args: {
className: undefined,
confirmationText:
- "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
- descriptionText: undefined,
+ "Fantastic! You're all set. Check the console for the data you submitted.",
+ descriptionText: "This is it.",
+ formHelperText: "You can do this.",
hiddenFields: { hiddenFields },
- id: "newsletterSignup-id",
+ id: undefined,
isInvalidEmail: false,
newsletterSignupType: "whatsOn",
onSubmit: undefined,
- title: undefined,
+ title: "The Newsletter Everyone Is Talking About",
view: "form",
},
parameters: {
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index a83b7e03be..d29bc6811e 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -39,7 +39,7 @@ interface NewsletterSignupProps {
* the initial/form view. Accepts a string or a JSX */
descriptionText?: string | JSX.Element;
/** Optional: Used to populate the Text component rendered below the input field. */
- formHelper?: string;
+ formHelperText?: string | JSX.Element;
/** A data object containing key/value pairs that will be added to the form
* field submitted data. */
hiddenFields?: any;
@@ -54,7 +54,7 @@ interface NewsletterSignupProps {
* include {email}, being the user's input, plus any objects passed via the hiddenValues prop.
*/
onSubmit: (values: { [key: string]: string }) => any;
- /** Used to populate the
header title. */
+ /** Used to populate the `
` header title. */
title: string;
/** Used to specify what is displayed in the component content area. */
view?: NewsletterSignupViewType;
@@ -74,6 +74,7 @@ export const NewsletterSignup = chakra(
className,
confirmationText = "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
descriptionText = "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
+ formHelperText,
hiddenFields,
id,
isInvalidEmail = false,
@@ -127,12 +128,12 @@ export const NewsletterSignup = chakra(
// @todo Is useEffect bad? I read that is not good. But I can't put any of this logic in the internalOnSubmit function
// because it won't work and I am not knowledgeable enough to figure out an alternate solution. So this is adapted from FeebackBox.
useEffect(() => {
- console.log("useEffect");
let timer;
if (buttonClicked) {
// If the consuming app does not provide any updates based
// on its API response, go to confirmation screen.
timer = setTimeout(() => {
+ setButtonClicked(false);
setViewType("confirmation");
}, 3000);
// If the consuming app does pass the API response to the
@@ -188,13 +189,13 @@ export const NewsletterSignup = chakra(
{formView && (
<>
- {/**/}
setEmail(e.target.value)} // e.target.value is what the user has input. So when they hit "submit" it will be stored in whatever variable we wish in the setEmail function.
placeholder="Enter your email address here"
@@ -264,14 +265,4 @@ export const NewsletterSignup = chakra(
)
);
-export function useNewsletterSignup() {
- const InternalNewsletterSignup = chakra((props) => {
- return ;
- });
-
- return {
- NewsletterSignup: InternalNewsletterSignup,
- };
-}
-
export default NewsletterSignup;
diff --git a/src/theme/components/newsletterSignup.ts b/src/theme/components/newsletterSignup.ts
index 4553b7fe49..3e88ce057b 100644
--- a/src/theme/components/newsletterSignup.ts
+++ b/src/theme/components/newsletterSignup.ts
@@ -6,6 +6,7 @@ const NewsLetterSignup = {
width: "100%",
"div#info": {
alignItems: "flex-start",
+ alignSelf: "stretch",
flex: "1 1 0",
bg: "ui.bg.default",
},
@@ -15,9 +16,9 @@ const NewsLetterSignup = {
"div#pitch": {
width: { base: "100%", md: "50%" }, // It's a two-column layout >md
padding: {
- base: "s l l l",
+ base: "var(--nypl-space-s) var(--nypl-space-l) var(--nypl-space-l) var(--nypl-space-l)", // @todo For some reason using "padding:" with more than one shorthand value, e.g. l, sm, xxl, doesn't work.
md: "l",
- lg: "l xxl l xl",
+ lg: "var(--nypl-space-l) var(--nypl-space-xxl) var(--nypl-space-l) var(--nypl-space-xl)",
}, // md
alignItems: "flex-start",
gap: "xs",
@@ -35,7 +36,7 @@ const NewsLetterSignup = {
fontWeight: "caption",
},
"div#action": {
- padding: { base: "l", lg: "l xxl" },
+ padding: { base: "l", lg: "var(--nypl-space-l) var(--nypl-space-xxl)" },
width: { base: "100%", md: "50%" }, // It's a two-column layout >md
},
form: {
@@ -47,7 +48,7 @@ const NewsLetterSignup = {
},
input: {
display: "flex",
- padding: "xs s",
+ padding: "var(--nypl-space-xs) var(--nypl-space-s)",
flexDirection: "column",
alignItems: "flex-start",
gap: "10px",
From 5983419b347127880013e5f4645ede124a72b687 Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Tue, 26 Sep 2023 16:07:49 -0400
Subject: [PATCH 09/39] NewsletterSignup Tests Round 1
---
.../NewsletterSignup.test.tsx | 523 ++--
.../__snapshots__/FeedbackBox.test.tsx.snap | 17 -
.../NewsletterSignup.test.tsx.snap | 2783 +++++++++++++++++
3 files changed, 3026 insertions(+), 297 deletions(-)
delete mode 100644 src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
create mode 100644 src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap
diff --git a/src/components/NewsletterSignup/NewsletterSignup.test.tsx b/src/components/NewsletterSignup/NewsletterSignup.test.tsx
index b171f25af6..3a4fe0635f 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.test.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.test.tsx
@@ -1,334 +1,297 @@
import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
+//import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import * as React from "react";
import renderer from "react-test-renderer";
import NewsletterSignup from "./NewsletterSignup";
+import { sectionDataMap } from "../../helpers/types";
describe("NewsletterSignup Accessibility", () => {
- it("passes axe accessibility when closed", async () => {
- const onSubmit = jest.fn();
+ const onSubmit = jest.fn();
+ it("Form state passes accessibility", async () => {
const { container } = render(
-
+
);
expect(await axe(container)).toHaveNoViolations();
});
- it("passes axe accessibility when opened", async () => {
- const onSubmit = jest.fn();
+ it("Error state passes accessibility", async () => {
const { container } = render(
-
+
);
-
- expect(screen.queryByText(/Comment/i)).not.toBeInTheDocument();
-
- screen.getByText("Help and Feeback").click();
- // Just to make sure the dialog is opened.
- expect(screen.getByText(/Comment/i)).toBeInTheDocument();
expect(await axe(container)).toHaveNoViolations();
});
-});
-
-describe("NewsletterSignup", () => {
- let onSubmit = jest.fn();
-
- it("renders a button component", () => {
- render();
-
- expect(
- screen.getByRole("button", { name: "Help and Feedback" })
- ).toBeInTheDocument();
- });
-
- it("renders the basic content when opened", () => {
- render();
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(screen.getByTestId("title")).toHaveTextContent("Help and Feedback");
- expect(
- screen.getByRole("textbox", { name: /comment/i })
- ).toBeInTheDocument();
- expect(screen.getByText(/500 characters remaining/i)).toBeInTheDocument();
- expect(screen.getByRole("link")).toHaveTextContent("Privacy Policy");
- // Does not render the radio group or email fields:
- expect(
- screen.queryByText(/What is your feedback about/i)
- ).not.toBeInTheDocument();
- expect(screen.queryByLabelText(/email/i)).not.toBeInTheDocument();
-
- expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
- });
-
- it("renders optional radio group and email field", () => {
- render(
+ it("Confirmation state passes accessibility", async () => {
+ const { container } = render(
);
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(
- screen.getByText(/what is your feedback about/i)
- ).toBeInTheDocument();
- expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
+ expect(await axe(container)).toHaveNoViolations();
});
- it("sets the invalid state for the comment and email field", () => {
- render(
+ it("Bad email state passes accessibility", async () => {
+ const { container } = render(
);
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(screen.getByText(/please fill out this field/i)).toBeInTheDocument();
- expect(
- screen.getByText(/please enter a valid email address/i)
- ).toBeInTheDocument();
+ expect(await axe(container)).toHaveNoViolations();
});
+});
- it("renders optional additional description text", () => {
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(
- screen.getByText(/Please share your question or feedback/i)
- ).toBeInTheDocument();
- });
+describe("NewsletterSignup Unit Tests", () => {
+ // If you want to see what's happening, insert below render()
+ // screen.debug();
+ let onSubmit = jest.fn();
+ describe("Renders the Minimum Required Elements", () => {
+ it("Renders the form", () => {
+ render();
+ expect(screen.getByRole("form")).toBeInTheDocument();
+ });
+ it("Renders the input textbox", () => {
+ render();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ });
+ it("Renders the button", () => {
+ render();
+ expect(
+ screen.getByRole("button", { name: "Submit" })
+ ).toBeInTheDocument();
+ });
+ it("Renders the title", () => {
+ render();
+ expect(screen.getByText(/Testing/i)).toBeInTheDocument();
+ });
+ }); // Close minimum elements tests
- it("renders optional notification text or JSX", () => {
- const { rerender } = render(
+ describe("Renders the Optional descriptionText and formHelperText Values", () => {
+ // Note: newsletterSignupType tests are covered in the snapshot tests below.
+ const testNewsletterSignup = (
);
+ it("Renders the description", () => {
+ render(testNewsletterSignup);
+ expect(screen.getByText(/Do not send cash./i)).toBeInTheDocument();
+ });
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(screen.getByText(/Call Number: JFE 95-8555/i)).toBeInTheDocument();
+ it("Renders the helper text", () => {
+ render(testNewsletterSignup);
+ expect(screen.getByText(/Just trying to help/i)).toBeInTheDocument();
+ });
+ }); // Close optional text elements test
+
+ describe("Renders the Feedback Views", () => {
+ it("Renders the error view", () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(/Oops! Something went wrong./i)
+ ).toBeInTheDocument();
+ expect(screen.getByTitle("errorFilled icon")).toBeInTheDocument();
+ });
- rerender(
- JSX notification}
- title="Help and Feedback"
- onSubmit={onSubmit}
- />
- );
+ it("Renders the confirmation view", () => {
+ render(
+
+ );
+ expect(screen.getByText(/Fantastic/i)).toBeInTheDocument();
+ expect(
+ screen.getByTitle("actionCheckCircleFilled icon")
+ ).toBeInTheDocument();
+ });
- expect(screen.getByTestId("paragraph")).toHaveTextContent(
- /jsx notification/i
- );
+ it("Renders the bad email view", () => {
+ render(
+
+ );
+ expect(
+ screen.getByText(/Please enter a valid email address./i)
+ ).toBeInTheDocument();
+ expect(screen.getByRole("form")).toBeInTheDocument();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ });
+ }); // Close feedback tests
+
+ // @todo These will likely need to be rewritten under the new specs
+ // describe("Behaviors on Button Click", () => {
+ // it("Disables the button when clicked", () => {
+ // render();
+ // //screen.debug();
+ // const button = screen.getByRole("button", { name: "Submit" });
+ // expect(button).not.toHaveAttribute("disabled");
+ // button.click();
+ // //screen.debug();
+ // expect(button).toHaveAttribute("");
+ // });
+ //
+ // it("Adds hidden fields data to the submitted data", () => {
+ // const hiddenFields = {
+ // "hidden-field-1": "hidden-field-value-1",
+ // "hidden-field-2": "hidden-field-value-2",
+ // };
+ // let submittedValues;
+ // let onSubmit = (values) => {
+ // submittedValues = values;
+ // };
+ // render(
+ //
+ // );
+ //
+ // const emailField = screen.getByLabelText(/email/i);
+ // userEvent.type(emailField, "email@email.com");
+ //
+ // const button = screen.getByRole("button", { name: "Submit" });
+ //
+ // button.click();
+ //
+ // expect(submittedValues).toEqual({
+ // email: "email@email.com",
+ // "hidden-field-1": "hidden-field-value-1",
+ // "hidden-field-2": "hidden-field-value-2",
+ // });
+ // });
+ // }); // Close behaviors on button click
+}); // Close unit tests.
+
+describe("NewsletterSignup Snapshots", () => {
+ const onSubmit = jest.fn();
+ it("Renders the default form UI snapshot correctly", () => {
+ const view = renderer
+ .create()
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("renders the `confirmation` screen through the `view` prop", () => {
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(
- screen.getByText(/thank you for submitting your feedback/i)
- ).toBeInTheDocument();
- expect(
- screen.queryByText(
- /if you provided an email address and require a response/i
+ it("Renders the form UI with formHelperText snapshot correctly", () => {
+ const view = renderer
+ .create(
+
)
- ).not.toBeInTheDocument();
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("renders the email `confirmation` message when showEmailField is true", () => {
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(
- screen.getByText(
- /if you provided an email address and require a response/i
+ it("Renders the form UI with description snapshot correctly", () => {
+ const view = renderer
+ .create(
+
)
- ).toBeInTheDocument();
- });
-
- it("renders the `error` screen through the `view` prop", () => {
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- expect(screen.getByText(/oops! something went wrong/i)).toBeInTheDocument();
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("submits the form and returns the submitted data", () => {
- let submittedValues;
- let onSubmit = (values) => {
- submittedValues = values;
- };
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- // The first comment field is the radio button.
- const commentField = screen.getAllByLabelText(/comment/i)[1];
- const emailField = screen.getByLabelText(/email/i);
- const submit = screen.getByRole("button", { name: "Submit" });
-
- screen.getByText(/bug/i).click();
- userEvent.type(commentField, "This is a comment");
- userEvent.type(emailField, "email@email.com");
-
- submit.click();
-
- expect(submittedValues).toEqual({
- category: "bug",
- comment: "This is a comment",
- email: "email@email.com",
- });
+ it("Renders the bad email UI snapshot correctly", () => {
+ const view = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("adds hidden fields data to the submitted data", () => {
- const hiddenFields = {
- "hidden-field-1": "hidden-field-value-1",
- "hidden-field-2": "hidden-field-value-2",
- };
- let submittedValues;
- let onSubmit = (values) => {
- submittedValues = values;
- };
- render(
-
- );
-
- const button = screen.getByRole("button", { name: "Help and Feedback" });
-
- button.click();
-
- // The first comment field is the radio button.
- const commentField = screen.getAllByLabelText(/comment/i)[1];
- const emailField = screen.getByLabelText(/email/i);
- const submit = screen.getByRole("button", { name: "Submit" });
-
- screen.getByText(/bug/i).click();
- userEvent.type(commentField, "This is a comment");
- userEvent.type(emailField, "email@email.com");
-
- submit.click();
-
- expect(submittedValues).toEqual({
- category: "bug",
- comment: "This is a comment",
- email: "email@email.com",
- "hidden-field-1": "hidden-field-value-1",
- "hidden-field-2": "hidden-field-value-2",
- });
+ it("Renders the error UI snapshot correctly", () => {
+ const view = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("transitions to the `form` screen from the `error` screen", () => {
- render(
-
- );
-
- // Open the dialog.
- screen.queryByRole("button", { name: "Help and Feedback" }).click();
-
- const button = screen.queryByRole("button", { name: "Try Again" });
- expect(
- screen.queryByText(/oops! something went wrong/i)
- ).toBeInTheDocument();
-
- button.click();
-
- // The `error` screen should no longer display.
- expect(
- screen.queryByText(/oops! something went wrong/i)
- ).not.toBeInTheDocument();
- expect(button).not.toBeInTheDocument();
-
- // We are back at the `form` screen.
- expect(
- screen.getByRole("textbox", { name: /comment/i })
- ).toBeInTheDocument();
- expect(screen.getByText(/500 characters remaining/i)).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ it("Renders the default confirmation UI snapshot correctly", () => {
+ const view = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(view).toMatchSnapshot();
});
- it("renders the UI snapshot correctly", () => {
- const basic = renderer
- .create()
+ it("Renders the confirmation UI with confirmationText snapshot correctly", () => {
+ const view = renderer
+ .create(
+
+ )
.toJSON();
-
- expect(basic).toMatchSnapshot();
+ expect(view).toMatchSnapshot();
});
-});
+
+ describe("Renders each color for each newsletterSignupType correctly", () => {
+ // newsletterSignupType values are determined by the types contained in the sectionDataMap. So it is safe to use the map directly.
+ sectionDataMap.map((section) => {
+ it(
+ "Renders " +
+ section.type +
+ " color band with " +
+ section.colorVals.primary,
+ () => {
+ const view = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(view).toMatchSnapshot();
+ }
+ );
+ });
+ }); // Close colors snapshots
+}); // Close snapshots
diff --git a/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
deleted file mode 100644
index 8f05d040d0..0000000000
--- a/src/components/NewsletterSignup/__snapshots__/FeedbackBox.test.tsx.snap
+++ /dev/null
@@ -1,17 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`NewsletterSignup renders the UI snapshot correctly 1`] = `
-
-
-
-`;
diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap
new file mode 100644
index 0000000000..f4070650ef
--- /dev/null
+++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap
@@ -0,0 +1,2783 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders blogs color band with section.blogs.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders booksAndMore color band with section.books-and-more.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders brand color band with brand.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders connect color band with section.connect.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders education color band with section.education.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders locations color band with section.locations.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders research color band with section.research.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders researchLibraryLpa color band with section.research-library.lpa 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders researchLibrarySchomburg color band with section.research-library.schomburg 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders researchLibrarySchwarzman color band with section.research-library.schwartzman 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignupType correctly Renders whatsOn color band with section.whats-on.primary 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+ Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.
+
+
+
+
+`;
+
+exports[`NewsletterSignup Snapshots Renders the default form UI snapshot correctly 1`] = `
+
+
+
+
+
+
+
+ Testing
+
+
+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.
+
+`;
From 3bd8d9deb558c65de5bc0a5cab269c730cdde23e Mon Sep 17 00:00:00 2001
From: Andrew Arnold
Date: Tue, 26 Sep 2023 16:08:47 -0400
Subject: [PATCH 10/39] NewsletterSignup w/o 3-second Timer
---
.../NewsletterSignup.stories.tsx | 25 +++++---
.../NewsletterSignup/NewsletterSignup.tsx | 59 +++----------------
2 files changed, 26 insertions(+), 58 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index d97b683ca0..6d60c80f24 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -69,25 +69,34 @@ const NewsletterSignupWithControls = (args) => {
args.confirmationText
);
const onSubmit = (values?: { [key: string]: string }) => {
+ let timer = setTimeout(() => {
+ setView("confirmation");
+ setConfirmationText(
+ "This is going to change your life. Check out those values in the console!"
+ );
+ }, 3000);
switch (values.email) {
case "":
setIsInvalidEmail(true);
+ clearTimeout(timer);
break;
case "error@nypl.org":
setView("error");
- break;
- case "confirmation@nypl.org":
- setView("confirmation");
- setConfirmationText(
- "This is going to change your life. Check out those values in the console!"
- );
+ clearTimeout(timer);
break;
}
- console.log("Submitted values:", values, isInvalidEmail);
+ console.log(
+ "Submitted values:",
+ values,
+ "isInvalidEmail: ",
+ isInvalidEmail,
+ "View: ",
+ view
+ );
};
return (
{
- // We want to keep internal state for the view but also
- // update if the consuming app updates it, based on API
- // success and failure responses.
- const [viewType, setViewType] = useStateWithDependencies(view);
- const [badEmail, setBadEmail] = useStateWithDependencies(isInvalidEmail);
+ const viewType = view;
const [buttonClicked, setButtonClicked] = useState(false);
const [email, setEmail] = useState("");
@@ -121,44 +116,6 @@ export const NewsletterSignup = chakra(
);
- // When the submit button is clicked, set a timeout before displaying
- // the confirmation or error screen. This automatically goes to the
- // confirmation view after three (3) seconds, but the consuming app
- // can set the error view if there are any issues.
- // @todo Is useEffect bad? I read that is not good. But I can't put any of this logic in the internalOnSubmit function
- // because it won't work and I am not knowledgeable enough to figure out an alternate solution. So this is adapted from FeebackBox.
- useEffect(() => {
- let timer;
- if (buttonClicked) {
- // If the consuming app does not provide any updates based
- // on its API response, go to confirmation screen.
- timer = setTimeout(() => {
- setButtonClicked(false);
- setViewType("confirmation");
- }, 3000);
- // If the consuming app does pass the API response to the
- // component, then cancel the timeout above and display the
- // appropriate screen.
- if (view !== viewType || isInvalidEmail !== badEmail) {
- setBadEmail(false); // @todo It won't let you resubmit.
- setButtonClicked(false);
- setViewType(view);
- clearTimeout(timer);
- }
- }
-
- return () => clearTimeout(timer);
- }, [
- buttonClicked,
- badEmail,
- errorView,
- isInvalidEmail,
- setBadEmail,
- setViewType,
- view,
- viewType,
- ]);
-
return (
-
+ setEmail(e.target.value)} // e.target.value is what the user has input. So when they hit "submit" it will be stored in whatever variable we wish in the setEmail function.
placeholder="Enter your email address here"
type="email"
value={email}
/>
-
+
+
+
+ Submit
+
+
+
)}
-
- {/* Error Screen */}
- {errorView && (
- <>
-
+
+ {confirmationText}
+
+ )}
+ {view === "error" && (
+
+
-
- Oops! Something went wrong.
-
- >
+ name="errorFilled"
+ size="large"
+ />
+ Oops! Something went wrong.
+
)}
- {/* End action div */}
);
}
From 747b451d21821c2a83eaaac3c2fbf5d1e6ff96c0 Mon Sep 17 00:00:00 2001
From: William Luisi
Date: Tue, 26 Sep 2023 21:57:13 -0400
Subject: [PATCH 12/39] more code cleanup
---
src/components/NewsletterSignup/NewsletterSignup.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index 276f66b152..8f793aa066 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -83,11 +83,11 @@ export const NewsletterSignup = chakra(
const styles = useStyleConfig("NewsletterSignup", {});
const iconColor = useColorModeValue(null, "dark.ui.typography.body");
- const displayForm = view === "form" || view === "submitting";
+ const isFormView = view === "form" || view === "submitting";
- // Manage focus to ensure accessibility when cofirmation or error message is rendered.
+ // Manage focus to ensure accessibility when confirmation or error message is rendered.
const focusRef = React.useRef(null);
- // When view changes, set focus to the confirmation or error content.
+ // When view prop changes, set focus to the confirmation or error content element.
React.useEffect(() => {
focusRef.current?.focus();
}, [view]);
@@ -122,7 +122,7 @@ export const NewsletterSignup = chakra(
- {displayForm && (
+ {isFormView && (
Date: Thu, 28 Sep 2023 13:31:23 -0400
Subject: [PATCH 13/39] NewsletterSignup Update w/ Testing
---
.../NewsletterSignup.stories.tsx | 79 +++---
.../NewsletterSignup.test.tsx | 243 ++++++++++--------
.../NewsletterSignup/NewsletterSignup.tsx | 58 ++---
.../NewsletterSignup.test.tsx.snap | 237 ++++++++++++++---
src/helpers/getSectionColors.ts | 3 +-
5 files changed, 418 insertions(+), 202 deletions(-)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
index f1cd1efe0e..5cc7b807aa 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx
@@ -12,13 +12,13 @@ const meta: Meta = {
},
argTypes: {
className: { control: false },
- id: { control: false },
confirmationText: {
control: "text",
table: {
defaultValue: {
summary:
- "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
+ "Thank you! You have successfully subscribed to our email updates! You can update your email subscription " +
+ "preferences at any time using the links at the bottom of the email.",
},
},
},
@@ -27,13 +27,15 @@ const meta: Meta = {
table: {
defaultValue: {
summary:
- "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
+ "Stay connected with the latest research news from NYPL, including information about our events, programs, " +
+ "exhibitions, and collections.",
},
},
},
formHelperText: {
control: "text",
},
+ id: { control: false },
newsletterSignupType: {
control: "select",
table: {
@@ -42,6 +44,13 @@ const meta: Meta = {
},
},
},
+ onChange: { control: false },
+ onSubmit: { control: false },
+ title: {
+ control: "text",
+ table: { defaultValue: { summary: "Sign Up for Our Newsletter!" } },
+ },
+ valueEmail: { control: false },
view: {
control: "select",
table: {
@@ -50,10 +59,6 @@ const meta: Meta = {
},
},
},
- title: {
- control: "text",
- table: { defaultValue: { summary: "Sign Up for Our Newsletter!" } },
- },
},
};
@@ -62,34 +67,51 @@ type Story = StoryObj;
const NewsletterSignupWithControls = (args) => {
const [view, setView] = useStateWithDependencies(args.view);
+ const [isInvalidEmail, setIsInvalidEmail] = useStateWithDependencies(
+ args.isInvalidEmail
+ );
- function handleSubmit(event: React.FormEvent): void {
+ function handleSubmit(event): void {
event.preventDefault();
-
setView("submitting");
-
- // Add short delay to better show the state changes.
- setTimeout(() => {
- setView("confirmation");
- }, 4000);
+ const userEmail = event.target.email.value;
+ switch (userEmail) {
+ case "error@nypl.org":
+ setView("error");
+ break;
+ case "bad@nypl.org":
+ setView("form");
+ setIsInvalidEmail(true);
+ break;
+ default:
+ // Add short delay to demonstrate the "submitted" state.
+ setTimeout(() => {
+ setView("confirmation");
+ }, 3000);
+ }
+ console.log("Submitted email: ", userEmail);
}
return (
);
};
-// Example hidden field values.
-const hiddenFields = {
- "hidden-field-1": "hidden-field-value-1",
- "hidden-field-2": "hidden-field-value-2",
-};
+// Example values.
+const title = "The Life-changing Newsletter";
+const descriptionText =
+ "This bespoke newsletter contains only those things that are critical for YOU to know, but that you either forgot, " +
+ "or had not been informed about. IMPORTANT: if you use error@nypl.org as the address, you will get the error screen. " +
+ "If you use bad@nypl.org you will get the invalid email screen.";
+const confirmationText =
+ "Fantastic! You're all set. Check the console for the data you submitted.";
+const formHelperText =
+ "Now, just put your email in that space up there and push that blue button.";
/**
* Main Story for the NewsletterSignup component. This must contains the `args`
@@ -98,17 +120,16 @@ const hiddenFields = {
export const WithControls: Story = {
args: {
className: undefined,
- confirmationText:
- "Fantastic! You're all set. Check the console for the data you submitted.",
- descriptionText:
- "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
- formHelperText: "You can do this.",
- hiddenFields: { hiddenFields },
+ confirmationText, // Shorthand. Value defined above.
+ descriptionText,
+ emailValue: "",
+ formHelperText,
id: undefined,
isInvalidEmail: false,
newsletterSignupType: "whatsOn",
+ onChange: undefined,
onSubmit: undefined,
- title: "Sign Up for Our Newsletter",
+ title,
view: "form",
},
parameters: {
diff --git a/src/components/NewsletterSignup/NewsletterSignup.test.tsx b/src/components/NewsletterSignup/NewsletterSignup.test.tsx
index 3a4fe0635f..87fd8d40fb 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.test.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.test.tsx
@@ -3,49 +3,91 @@ import { render, screen } from "@testing-library/react";
import { axe } from "jest-axe";
import * as React from "react";
import renderer from "react-test-renderer";
-
import NewsletterSignup from "./NewsletterSignup";
import { sectionDataMap } from "../../helpers/types";
+// If you want to see what's happening, insert below render()
+// screen.debug();
+
describe("NewsletterSignup Accessibility", () => {
- const onSubmit = jest.fn();
- it("Form state passes accessibility", async () => {
+ const onSubmit = jest.fn(),
+ onChange = jest.fn(),
+ valueEmail = "";
+ it("Form state w/ all optional props (displayed and undisplayed) passes accessibility", async () => {
const { container } = render(
);
expect(await axe(container)).toHaveNoViolations();
});
- it("Error state passes accessibility", async () => {
+ it("Submitting state w/ all optional props (displayed and undisplayed) passes accessibility", async () => {
const { container } = render(
-
+
+ );
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it("Error state w/ all optional props (displayed and undisplayed) passes accessibility", async () => {
+ const { container } = render(
+
);
expect(await axe(container)).toHaveNoViolations();
});
- it("Confirmation state passes accessibility", async () => {
+ it("Confirmation state w/ all optional props (displayed and undisplayed) passes accessibility", async () => {
const { container } = render(
);
expect(await axe(container)).toHaveNoViolations();
});
- it("Bad email state passes accessibility", async () => {
+ it("Bad email state w/ all optional props (displayed and undisplayed) passes accessibility", async () => {
const { container } = render(
);
expect(await axe(container)).toHaveNoViolations();
@@ -53,55 +95,56 @@ describe("NewsletterSignup Accessibility", () => {
});
describe("NewsletterSignup Unit Tests", () => {
- // If you want to see what's happening, insert below render()
- // screen.debug();
- let onSubmit = jest.fn();
- describe("Renders the Minimum Required Elements", () => {
- it("Renders the form", () => {
- render();
- expect(screen.getByRole("form")).toBeInTheDocument();
- });
- it("Renders the input textbox", () => {
- render();
- expect(screen.getByRole("textbox")).toBeInTheDocument();
- });
- it("Renders the button", () => {
- render();
- expect(
- screen.getByRole("button", { name: "Submit" })
- ).toBeInTheDocument();
- });
- it("Renders the title", () => {
- render();
- expect(screen.getByText(/Testing/i)).toBeInTheDocument();
- });
- }); // Close minimum elements tests
+ /** Notes
+ *
+ * 1. The newsletterSignupType tests are covered in the snapshot tests below.
+ * 2. Because the functionality of the submit click is handled entirely by the consuming app, there seems to be no
+ * way to test that the component does what it should when the button is clicked.
+ */
- describe("Renders the Optional descriptionText and formHelperText Values", () => {
- // Note: newsletterSignupType tests are covered in the snapshot tests below.
+ const onSubmit = jest.fn(),
+ onChange = jest.fn(),
+ valueEmail = "";
+ it("Renders the Minimum Required Elements for the Form", () => {
+ render(
+
+ );
+ expect(screen.getByRole("form")).toBeInTheDocument();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Submit" })).toBeInTheDocument();
+ expect(screen.getByText(/Testing/i)).toBeInTheDocument();
+ expect(screen.getByText(/Privacy Policy/i)).toBeInTheDocument();
+ });
+
+ it("Renders the Optional descriptionText and formHelperText Values for the Form", () => {
const testNewsletterSignup = (
);
- it("Renders the description", () => {
- render(testNewsletterSignup);
- expect(screen.getByText(/Do not send cash./i)).toBeInTheDocument();
- });
-
- it("Renders the helper text", () => {
- render(testNewsletterSignup);
- expect(screen.getByText(/Just trying to help/i)).toBeInTheDocument();
- });
- }); // Close optional text elements test
+ render(testNewsletterSignup);
+ expect(screen.getByText(/Do not send cash./i)).toBeInTheDocument();
+ expect(screen.getByText(/Just trying to help/i)).toBeInTheDocument();
+ });
describe("Renders the Feedback Views", () => {
it("Renders the error view", () => {
render(
-
+
);
expect(
screen.getByText(/Oops! Something went wrong./i)
@@ -113,8 +156,9 @@ describe("NewsletterSignup Unit Tests", () => {
render(
);
@@ -129,8 +173,9 @@ describe("NewsletterSignup Unit Tests", () => {
);
expect(
@@ -141,57 +186,21 @@ describe("NewsletterSignup Unit Tests", () => {
expect(screen.getByRole("button")).toBeInTheDocument();
});
}); // Close feedback tests
-
- // @todo These will likely need to be rewritten under the new specs
- // describe("Behaviors on Button Click", () => {
- // it("Disables the button when clicked", () => {
- // render();
- // //screen.debug();
- // const button = screen.getByRole("button", { name: "Submit" });
- // expect(button).not.toHaveAttribute("disabled");
- // button.click();
- // //screen.debug();
- // expect(button).toHaveAttribute("");
- // });
- //
- // it("Adds hidden fields data to the submitted data", () => {
- // const hiddenFields = {
- // "hidden-field-1": "hidden-field-value-1",
- // "hidden-field-2": "hidden-field-value-2",
- // };
- // let submittedValues;
- // let onSubmit = (values) => {
- // submittedValues = values;
- // };
- // render(
- //
- // );
- //
- // const emailField = screen.getByLabelText(/email/i);
- // userEvent.type(emailField, "email@email.com");
- //
- // const button = screen.getByRole("button", { name: "Submit" });
- //
- // button.click();
- //
- // expect(submittedValues).toEqual({
- // email: "email@email.com",
- // "hidden-field-1": "hidden-field-value-1",
- // "hidden-field-2": "hidden-field-value-2",
- // });
- // });
- // }); // Close behaviors on button click
}); // Close unit tests.
describe("NewsletterSignup Snapshots", () => {
- const onSubmit = jest.fn();
+ const onSubmit = jest.fn(),
+ onChange = jest.fn(),
+ valueEmail = "";
it("Renders the default form UI snapshot correctly", () => {
const view = renderer
- .create()
+ .create(
+
+ )
.toJSON();
expect(view).toMatchSnapshot();
});
@@ -200,8 +209,9 @@ describe("NewsletterSignup Snapshots", () => {
const view = renderer
.create(
)
@@ -213,8 +223,9 @@ describe("NewsletterSignup Snapshots", () => {
const view = renderer
.create(
)
@@ -226,8 +237,9 @@ describe("NewsletterSignup Snapshots", () => {
const view = renderer
.create(
)
@@ -235,21 +247,41 @@ describe("NewsletterSignup Snapshots", () => {
expect(view).toMatchSnapshot();
});
- it("Renders the error UI snapshot correctly", () => {
+ it("Renders the submitting state snapshot correctly", () => {
+ const view = renderer
+ .create(
+
+ )
+ .toJSON();
+ expect(view).toMatchSnapshot();
+ });
+
+ it("Renders the error state snapshot correctly", () => {
const view = renderer
.create(
-
+
)
.toJSON();
expect(view).toMatchSnapshot();
});
- it("Renders the default confirmation UI snapshot correctly", () => {
+ it("Renders the default confirmation state snapshot correctly", () => {
const view = renderer
.create(
)
@@ -261,8 +293,9 @@ describe("NewsletterSignup Snapshots", () => {
const view = renderer
.create(
@@ -272,7 +305,8 @@ describe("NewsletterSignup Snapshots", () => {
});
describe("Renders each color for each newsletterSignupType correctly", () => {
- // newsletterSignupType values are determined by the types contained in the sectionDataMap. So it is safe to use the map directly.
+ // The newsletterSignupType values are determined by the types contained in the sectionDataMap.
+ // So it is safe to use the map directly.
sectionDataMap.map((section) => {
it(
"Renders " +
@@ -283,8 +317,9 @@ describe("NewsletterSignup Snapshots", () => {
const view = renderer
.create(
)
diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx
index 8f793aa066..13c47f477d 100644
--- a/src/components/NewsletterSignup/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup/NewsletterSignup.tsx
@@ -20,37 +20,39 @@ import { getSectionColors } from "../../helpers/getSectionColors";
import { SectionTypes } from "../../helpers/types";
interface NewsletterSignupProps {
- /** Additional class name to add. */
+ /** Optional: Additional class name to add. */
className?: string;
- /** Used to add additional information to the default confirmation message in
- * the confirmation view. */
+ /** Optional: Used to override the default confirmation message in the confirmation view. Accepts a string or
+ * an element. */
confirmationText?: string | JSX.Element;
- /** Used to add description text above the form input fields in
- * the initial/form view. Accepts a string or a JSX */
+ /** Optional: Appears below the title to provide details about the newsletter. Accepts a string or an element. */
descriptionText?: string | JSX.Element;
- /** Optional: Used to populate the Text component rendered below the input field. */
+ /** Optional: Appears below the input field's example text to provide any additional instructions. Accepts a string or
+ * an element. */
formHelperText?: string | JSX.Element;
- /** A data object containing key/value pairs that will be added to the form
- * field submitted data. */
- hiddenFields?: any;
- /** ID that other components can cross-reference for accessibility purposes */
+ /** Optional: ID that other components can cross-reference for accessibility purposes */
id?: string;
- /** Toggles the invalid state for the email field. */
+ /** Optional: Toggles the invalid state for the email field. */
isInvalidEmail?: boolean;
- /* Optional value to determine the section color highlight */
+ /** Optional: Value to determine the section color highlight */
newsletterSignupType?: SectionTypes;
- /** A submit handler function that will be called when the form is submitted. */
+ /** Required: a handler function that will be called when the form is submitted. */
onSubmit: (event: React.FormEvent) => void;
- /** A on change handler function for the text input. */
- onChangeEmail: (event: React.ChangeEvent) => void;
- /** The value of the email text input field. */
+ /** Required: a handler function that will be called when the text input changes. */
+ onChange: (event: React.ChangeEvent) => void;
+ /** Optional: Used to populate the `
` header title. */
+ title?: string;
+ /** Required: the value of the email text input field. */
valueEmail: string;
- /** Used to populate the `
` header title. */
- title: string;
- /** Used to specify what is displayed in the component content area. */
+ /** Optional: Used to specify what is displayed in the component form/feedback area. */
view?: "form" | "submitting" | "confirmation" | "error";
}
+const defaultConfirmationText =
+ "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.";
+const defaultDescriptionText =
+ "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.";
+
/**
* The NewsletterSignup component provides a way for patrons to register for an
* email-based newsletter distribution list.
@@ -63,15 +65,14 @@ export const NewsletterSignup = chakra(
(
{
className,
- confirmationText = "Thank you! You have successfully subscribed to our email updates! You can update your email subscription preferences at any time using the links at the bottom of the email.",
- descriptionText = "Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.",
+ confirmationText = defaultConfirmationText,
+ descriptionText = defaultDescriptionText,
formHelperText,
- hiddenFields,
id,
isInvalidEmail = false,
newsletterSignupType = "whatsOn",
- onChangeEmail,
- valueEmail,
+ onChange,
+ valueEmail = "",
onSubmit,
title = "Sign Up for Our Newsletter!",
view = "form",
@@ -112,7 +113,7 @@ export const NewsletterSignup = chakra(
{descriptionText}
Submit
@@ -154,7 +154,6 @@ export const NewsletterSignup = chakra(
{view === "confirmation" && (
- Testing
+ Sign Up for Our Newsletter!