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. + + { + 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.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx new file mode 100644 index 0000000000..2f69a9718b --- /dev/null +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { withDesign } from "storybook-addon-designs"; + +import NewsletterSignup, { + newsletterSignupViewTypeArray, +} from "./NewsletterSignup"; +import useStateWithDependencies from "../../hooks/useStateWithDependencies"; + +const meta: Meta = { + title: "Components/Form Elements/NewsletterSignup", + component: NewsletterSignup, + decorators: [withDesign], + parameters: { + jest: ["NewsletterSignup.test.tsx"], + }, + argTypes: { + 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 } }, + onSubmit: { control: false }, + showCategoryField: { table: { defaultValue: { summary: false } } }, + showEmailField: { table: { defaultValue: { summary: true } } }, + view: { + control: { type: "select" }, + options: newsletterSignupViewTypeArray, + table: { defaultValue: { summary: "form" } }, + }, + }, +}; + +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", + }; + // 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 NewsletterSignup component. This must contains the `args` + * and `parameters` properties in this object. + */ +export const WithControls: Story = { + args: { + className: undefined, + confirmationText: "", + descriptionText: "Please share your question or feedback.", + hiddenFields: undefined, + id: "newsletterSignup-id", + isInvalidComment: false, + isInvalidEmail: false, + isOpen: undefined, + notificationText: undefined, + onClose: undefined, + onOpen: undefined, + onSubmit: undefined, + showCategoryField: false, + showEmailField: false, + title: "Help and Feedback", + view: "form", + }, + parameters: { + design: { + type: "figma", + url: "", + }, + jest: "NewsletterSignup.test.tsx", + }, + render: (args) => , +}; diff --git a/src/components/NewsletterSignup/NewsletterSignup.test.tsx b/src/components/NewsletterSignup/NewsletterSignup.test.tsx new file mode 100644 index 0000000000..b171f25af6 --- /dev/null +++ b/src/components/NewsletterSignup/NewsletterSignup.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"; + +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/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} + + + +
+ {/* Initial form Screen */} + {isFormView && ( + <> + + {(notificationElement || descriptionElement) && ( + <> + {notificationElement} + {descriptionElement} + + )} + {showCategoryField && ( + + setCategory(selected)} + > + + + + + + )} + + setComment(e.target.value)} + placeholder="Enter your question or feedback here" + type="textarea" + defaultValue={state.comment} + /> + + {showEmailField && ( + + setEmail(e.target.value)} + placeholder="Enter your email address here" + type="email" + value={state.email} + /> + + )} + + {privacyPolicyField} + + + + + + + + )} + + {/* Confirmation Screen */} + {isConfirmationView && ( + <> + + + + Thank you for submitting your feedback. + + {showEmailField && ( + + If you provided an email address and require a + response, our service staff will reach out to you + via email. + + )} + {confirmationText ? ( + {confirmationText} + ) : undefined} + + {privacyPolicyField} + + + + + + + )} + + {/* Error Screen */} + {isErrorView && ( + <> + + + + Oops! Something went wrong. An error occured while + processing your feedback. + + + {privacyPolicyField} + + + + + + + + )} +
+
+
+
+
+ ); + } + ) +); + +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} - - - -
- {/* Initial form Screen */} - {isFormView && ( + {/* Initial form Screen */} + {isFormView && ( + <> + + {(notificationElement || descriptionElement) && ( <> - - {(notificationElement || descriptionElement) && ( - <> - {notificationElement} - {descriptionElement} - - )} - {showCategoryField && ( - - setCategory(selected)} - > - - - - - - )} - - setComment(e.target.value)} - placeholder="Enter your question or feedback here" - type="textarea" - defaultValue={state.comment} - /> - - {showEmailField && ( - - setEmail(e.target.value)} - placeholder="Enter your email address here" - type="email" - value={state.email} - /> - - )} - - {privacyPolicyField} - - - - - - + {notificationElement} + {descriptionElement} )} - - {/* Confirmation Screen */} - {isConfirmationView && ( - <> - + setCategory(selected)} > - - - Thank you for submitting your feedback. - - {showEmailField && ( - - If you provided an email address and require a - response, our service staff will reach out to you - via email. - - )} - {confirmationText ? ( - {confirmationText} - ) : undefined} - - {privacyPolicyField} - - - - - - + + + + + )} + + setComment(e.target.value)} + placeholder="Enter your question or feedback here" + type="textarea" + defaultValue={state.comment} + /> + + {showEmailField && ( + + setEmail(e.target.value)} + placeholder="Enter your email address here" + type="email" + value={state.email} + /> + )} + + {privacyPolicyField} + + + + + + + + )} - {/* Error Screen */} - {isErrorView && ( - <> - - - - Oops! Something went wrong. An error occured while - processing your feedback. - - - {privacyPolicyField} - - - - - - - + {/* Confirmation Screen */} + {isConfirmationView && ( + <> + + + + Thank you for submitting your feedback. + + {showEmailField && ( + + If you provided an email address and require a response, + our service staff will reach out to you via email. + )} - -
-
-
+ {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} +
onSubmit(e)} + gap="grid.s" + maxW="415px" + w={{ base: "full" }} + > + + + { + onChange(e.target.value); + }} + /> + + + + + +
+ + + ); +} 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 ( - -
- {/* Initial form Screen */} - {isFormView && ( - <> - - {(notificationElement || descriptionElement) && ( - <> - {notificationElement} - {descriptionElement} - - )} - {showCategoryField && ( - - setCategory(selected)} - > - - - - - - )} - - setComment(e.target.value)} - placeholder="Enter your question or feedback here" - type="textarea" - defaultValue={state.comment} - /> - - {showEmailField && ( + + +

Colorbox

+
+ + + {descriptionElement && <>{descriptionElement}} + {privacyPolicy} + + + + {/* Initial form Screen */} + {isFormView && ( + <> + + {/*Email Field*/} setEmail(e.target.value)} placeholder="Enter your email address here" @@ -339,110 +236,69 @@ export const NewsletterSignup = chakra( value={state.email} /> - )} - - {privacyPolicyField} - - - - - - - - )} + + + + + + )} - {/* Confirmation Screen */} - {isConfirmationView && ( - <> - - - - Thank you for submitting your feedback. - - {showEmailField && ( - - If you provided an email address and require a response, - our service staff will reach out to you via email. + {/* Confirmation Screen */} + {isConfirmationView && ( + <> + + + + Thank you for submitting your feedback. - )} - {confirmationText ? ( - {confirmationText} - ) : undefined} - - {privacyPolicyField} - - - - - - - )} + {confirmationText ? ( + {confirmationText} + ) : undefined} + + + )} - {/* Error Screen */} - {isErrorView && ( - <> - - + - - Oops! Something went wrong. An error occured while - processing your feedback. - - - {privacyPolicyField} - - - + key="errorWrapper" + margin="auto" + tabIndex={0} + textAlign="center" + ref={focusRef} + > + + + Oops! Something went wrong. An error occured while + processing your feedback. + + + - - - - )} - -
+ + + )} + + + + // + //
+ // {/* Initial form Screen */} + // {isFormView && ( + // <> + // + // + // {descriptionElement && <>{descriptionElement}} + // {privacyPolicyField} + // {/*Email Field*/} + // + // setEmail(e.target.value)} + // placeholder="Enter your email address here" + // type="email" + // value={state.email} + // /> + // + // + // + // + // + // + // )} + // + // {/* Confirmation Screen */} + // {isConfirmationView && ( + // <> + // + // + // + // Thank you for submitting your feedback. + // + // {confirmationText ? ( + // {confirmationText} + // ) : undefined} + // + // {privacyPolicyField} + // + // )} + // + // {/* Error Screen */} + // {isErrorView && ( + // <> + // + // + // + // Oops! Something went wrong. An error occured while + // processing your feedback. + // + // + // {privacyPolicyField} + // + // + // + // + // )} + //
+ //
); } ) ); export function useNewsletterSignup() { - const { isOpen, onClose, onOpen } = useDisclosure(); const InternalNewsletterSignup = chakra((props) => { - return ( - - ); + return ; }); return { - isOpen, - onClose, - onOpen, NewsletterSignup: InternalNewsletterSignup, }; } diff --git a/src/helpers/getSectionColors.ts b/src/helpers/getSectionColors.ts new file mode 100644 index 0000000000..4365713824 --- /dev/null +++ b/src/helpers/getSectionColors.ts @@ -0,0 +1,20 @@ +/* Retrieves section color values. + * + * Requires a section type as defined by sectionDataMap. Accepts an optional color + * preference ("primary" or "secondary"). + * + * @returns A string if one color is requested or an object containing both (default). + */ +import { sectionDataMap, SectionTypes } from "./types"; + +export function getSectionColors( + type: SectionTypes, + colorVal?: "primary" | "secondary" +) { + let colorName = sectionDataMap.filter((section) => section.type === type)[0] + .colorVals; + if (colorVal) { + colorName = colorName[colorVal]; + } + return colorName; +} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 073ab4944c..d906d31c79 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -1,2 +1,102 @@ export const layoutTypesArray = ["column", "row"] as const; export type LayoutTypes = typeof layoutTypesArray[number]; + +/* The "Source of Truth" for NYPL's sections. + * + * For functionality purposes this list includes section types that are not, + * technically, sections, but are treated as such, e.g. "brand," and the + * research libraries. + */ +export const sectionDataMap = [ + { + type: "blogs", + title: "Blogs", + colorVals: { + primary: "section.blogs.primary", + secondary: "section.blogs.secondary", + }, + }, + { + type: "booksAndMore", + title: "Books and More", + colorVals: { + primary: "section.books-and-more.primary", + secondary: "section.books-and-more.secondary", + }, + }, + { + type: "brand", + title: "New York Public Library", + colorVals: { + primary: "brand.primary", + secondary: "brand.secondary", + }, + }, + { + type: "connect", + title: "Connect", + colorVals: { + primary: "section.connect.primary", + secondary: "section.connect.secondary", + }, + }, + { + type: "education", + title: "Education", + colorVals: { + primary: "section.education.primary", + secondary: "section.education.secondary", + }, + }, + { + type: "locations", + title: "Locations", + colorVals: { + primary: "section.locations.primary", + secondary: "section.locations.secondary", + }, + }, + { + type: "research", + title: "Research", + colorVals: { + primary: "section.research.primary", + secondary: "section.research.secondary", + }, + }, + { + type: "researchLibraryLpa", + title: "The New York Public Library for the Performing Arts", + colorVals: { + primary: "section.research-library.lpa", + secondary: null, + }, + }, + { + type: "researchLibrarySchomburg", + title: "Schomburg Center for Research in Black Culture", + colorVals: { + primary: "section.research-library.schomburg", + secondary: null, + }, + }, + { + type: "researchLibrarySchwarzman", + title: "Stephen A. Schwarzman Building", + colorVals: { + primary: "section.research-library.schwartzman", + secondary: null, + }, + }, + { + type: "whatsOn", + title: "What's On", + colorVals: { + primary: "section.whats-on.primary", + secondary: "section.whats-on.secondary", + }, + }, +] as const; + +export const sectionTypeArray = sectionDataMap.map(({ type }) => type); +export type SectionTypes = typeof sectionTypeArray[number]; From 735e4c7fc1c96ea3cecf2f6116a2d1bcd1d016ff Mon Sep 17 00:00:00 2001 From: Andrew Arnold Date: Wed, 20 Sep 2023 09:35:54 -0400 Subject: [PATCH 05/39] NewsletterSignup Functional Layout --- .../NewsletterSignup/NewsletterSignup.tsx | 252 ++++++++---------- src/theme/components/newsletterSignup.ts | 118 ++++---- 2 files changed, 174 insertions(+), 196 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index 3c36dff69c..92f5f7b86b 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -3,7 +3,7 @@ import { chakra, Stack, useColorModeValue, - useMultiStyleConfig, + useStyleConfig, VStack, } from "@chakra-ui/react"; import React, { forwardRef, useEffect, useRef, useState } from "react"; @@ -23,7 +23,6 @@ import { SectionTypes } from "../../helpers/types"; export const newsletterSignupViewTypeArray = [ "form", - "loading", "confirmation", "error", ] as const; @@ -39,12 +38,12 @@ 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. */ + /** Optional: Used to populate the Text component rendered below the input 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 that other components can cross-reference for accessibility purposes */ id?: string; /** Toggles the invalid state for the email field. */ isInvalidEmail?: boolean; @@ -55,8 +54,7 @@ interface NewsletterSignupProps { * values from the `hiddenFields` prop. */ onSubmit: (values: { [key: string]: string }) => any; - /** Used to populate the label on the open button and the `Drawer`'s - * header title. */ + /* Used to populate the

header title. */ title: string; /** Used to specify what screen should be displayed. */ view?: NewsletterSignupViewType; @@ -98,16 +96,15 @@ export const NewsletterSignup = chakra( const { isLargerThanMobile } = useNYPLBreakpoints(); const focusRef = useRef(); - const styles = useMultiStyleConfig("NewsletterSignup", {}); + const styles = useStyleConfig("NewsletterSignup", {}); const isFormView = viewType === "form"; const isConfirmationView = viewType === "confirmation"; const isErrorView = viewType === "error"; const confirmationTimeout = 3000; - - const initTemplateRows = "auto 1fr"; const iconColor = useColorModeValue(null, "dark.ui.typography.body"); - console.log(getSectionColors(newsletterSignupType)); + let buttonClicked = false; + // Unused since cancel button removed. Maybe useful later? // const closeAndResetForm = () => { // setViewType("form"); @@ -120,20 +117,14 @@ export const NewsletterSignup = chakra( submittedValues = { ...submittedValues, ...hiddenFields }; } onSubmit && onSubmit(submittedValues); - setIsSubmitted(true); + buttonClicked = true; }; - const descriptionElement = - isFormView && descriptionText ? ( - {descriptionText} - ) : undefined; - const privacyPolicy = ( Privacy Policy @@ -144,8 +135,10 @@ export const NewsletterSignup = chakra( // 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 (isSubmitted) { + if (buttonClicked) { // If the consuming app does not provide any updates based // on its API response, go to confirmation screen. timer = setTimeout(() => { @@ -168,8 +161,18 @@ export const NewsletterSignup = chakra( } } - return () => clearTimeout(timer); - }, [clearValues, isErrorView, isSubmitted, setViewType, view, viewType]); + 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, @@ -188,129 +191,106 @@ export const NewsletterSignup = chakra( - -

Colorbox

-
- - - {descriptionElement && <>{descriptionElement}} - {privacyPolicy} - - -
+ - {/* Initial form Screen */} - {isFormView && ( - <> - - {/*Email Field*/} - - setEmail(e.target.value)} - placeholder="Enter your email address here" - type="email" - value={state.email} - /> - - - - - - - )} - - {/* Confirmation Screen */} - {isConfirmationView && ( - <> - - + + + {descriptionText} + {privacyPolicy} + + + {/* Close info div */} + {/* Begin action div */} + + {/* Initial form Screen */} + {isFormView && ( + <> + + + setEmail(e.target.value)} + placeholder="Enter your email address here" + type="email" + value={state.email} /> - - Thank you for submitting your feedback. - - {confirmationText ? ( - {confirmationText} - ) : undefined} - - - )} - - {/* Error Screen */} - {isErrorView && ( - <> - - - - Oops! Something went wrong. An error occured while - processing your feedback. - - + - - )} - + + + )} + + {/* Confirmation Screen */} + {isConfirmationView && ( + <> + + + + Thank you for submitting your feedback. + + {confirmationText ? ( + {confirmationText} + ) : undefined} + + + )} + {/* Error Screen */} + {isErrorView && ( + <> + + + Oops! Something went wrong. + + + )}
+ {/* End action div */}
// //
// - // Oops! Something went wrong. An error occured while + // Oops! Something went wrong. An error occurred while // processing your feedback. // // diff --git a/src/theme/components/newsletterSignup.ts b/src/theme/components/newsletterSignup.ts index 1ef5492a31..4553b7fe49 100644 --- a/src/theme/components/newsletterSignup.ts +++ b/src/theme/components/newsletterSignup.ts @@ -1,70 +1,68 @@ -import { screenreaderOnly } from "./globalMixins"; - -const FeedbackBox = { - parts: [ - "closeButton", - "drawerBody", - "drawerContent", - "drawerHeader", - "openButton", - ], +const NewsLetterSignup = { 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", - }, - }, + alignItems: "center", + borderWidth: { base: "0 1px 1px 1px", md: "1px 1px 1px 0" }, + maxWidth: "1280px", + width: "100%", + "div#info": { + alignItems: "flex-start", + flex: "1 1 0", + bg: "ui.bg.default", + }, + "div#color-box": { + alignSelf: "stretch", + }, + "div#pitch": { + width: { base: "100%", md: "50%" }, // It's a two-column layout >md + padding: { + base: "s l l l", + md: "l", + lg: "l xxl l xl", + }, // md + alignItems: "flex-start", + gap: "xs", + flex: "1 0 0", + }, + "div#pitch, h3, #pitch>p, #pitch>a": { margin: "unset" }, + h3: { + letterSpacing: "unset", }, - 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" }, - }, + "a#privacy": { + display: "flex", + alignItems: "center", + gap: "xxs", + fontSize: "desktop.caption", + fontWeight: "caption", + }, + "div#action": { + padding: { base: "l", lg: "l xxl" }, + width: { base: "100%", md: "50%" }, // It's a two-column layout >md }, - drawerContent: { - marginStart: "auto", - width: { base: "100%", md: "375px" }, + form: { + width: "100%", }, - drawerHeader: { - alignItems: "baseline", - background: "ui.bg.hover", - borderBottomWidth: "1px", - borderLeftWidth: { base: undefined, md: "1px" }, - borderTopWidth: "1px", + "#newsletter-form-parent": { + gridTemplateColumns: { base: null, lg: "1fr 78px" }, // The button is 78px wide and must sit to the right of the input field >lg. + gap: { base: "s", lg: "xs" }, + }, + input: { 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", - }, + padding: "xs s", + flexDirection: "column", + alignItems: "flex-start", + gap: "10px", + alignSelf: "stretch", + borderRadius: "2px", + border: "1px solid ui.border.default", + background: "ui.white", + }, + "div#newsletterSignup-id-email-helperText": { + fontWeight: "400", }, - openButton: { - position: "fixed", - borderRadius: "0", - bottom: "0", - right: "0", - zIndex: "5", + button: { + marginTop: { base: null, lg: "31px" }, // The button must align w/ the input field, but using {align-items: center} doesn't quite work due to the input field not being the literal v-center. }, }, }; -export default FeedbackBox; +export default NewsLetterSignup; From 00f217b9d44c9c34d2a8b35011dffd9e609eda23 Mon Sep 17 00:00:00 2001 From: Andrew Arnold Date: Fri, 22 Sep 2023 12:47:57 -0400 Subject: [PATCH 06/39] NewsletterSignup w/o onSubmit funtionality --- .../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 - .../NewsletterSignup/NewsletterSignup.mdx | 47 +++ .../NewsletterSignup.stories.tsx | 51 ++- .../NewsletterSignup/NewsletterSignup.tsx | 279 ++++----------- .../useNewsletterSignupReducer.ts | 66 ---- src/hooks/useStateWithDependencies.ts | 3 + 13 files changed, 140 insertions(+), 1254 deletions(-) delete mode 100644 src/components/EmailSubscription/EmailSubscription.mdx delete mode 100644 src/components/EmailSubscription/EmailSubscription.stories.tsx delete mode 100644 src/components/EmailSubscription/EmailSubscription.test.tsx delete mode 100644 src/components/EmailSubscription/EmailSubscription.tsx delete mode 100644 src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx delete mode 100644 src/components/EmailSubscription/EmailSubscriptionForm.tsx delete mode 100644 src/components/EmailSubscription/EmailSubscriptionWrapper.tsx delete mode 100644 src/components/EmailSubscription/index.ts delete mode 100644 src/components/NewsletterSignup/useNewsletterSignupReducer.ts diff --git a/src/components/EmailSubscription/EmailSubscription.mdx b/src/components/EmailSubscription/EmailSubscription.mdx deleted file mode 100644 index 74bb80c243..0000000000 --- a/src/components/EmailSubscription/EmailSubscription.mdx +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 0af0c88363..0000000000 --- a/src/components/EmailSubscription/EmailSubscription.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 6060991293..0000000000 --- a/src/components/EmailSubscription/EmailSubscription.test.tsx +++ /dev/null @@ -1,334 +0,0 @@ -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 deleted file mode 100644 index cbdc6de1e5..0000000000 --- a/src/components/EmailSubscription/EmailSubscription.tsx +++ /dev/null @@ -1,269 +0,0 @@ -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 deleted file mode 100644 index 0c83558ed1..0000000000 --- a/src/components/EmailSubscription/EmailSubscriptionConfirmation.tsx +++ /dev/null @@ -1,68 +0,0 @@ -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 deleted file mode 100644 index d6e492da99..0000000000 --- a/src/components/EmailSubscription/EmailSubscriptionForm.tsx +++ /dev/null @@ -1,79 +0,0 @@ -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} - onSubmit(e)} - gap="grid.s" - maxW="415px" - w={{ base: "full" }} - > - - - { - onChange(e.target.value); - }} - /> - - - - - - - - - ); -} 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. + + ## 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 */} - // - // - // {/* Initial form Screen */} - // {isFormView && ( - // <> - // - // - // {descriptionElement && <>{descriptionElement}} - // {privacyPolicyField} - // {/*Email Field*/} - // - // setEmail(e.target.value)} - // placeholder="Enter your email address here" - // type="email" - // value={state.email} - // /> - // - // - // - // - // - // - // )} - // - // {/* Confirmation Screen */} - // {isConfirmationView && ( - // <> - // - // - // - // Thank you for submitting your feedback. - // - // {confirmationText ? ( - // {confirmationText} - // ) : undefined} - // - // {privacyPolicyField} - // - // )} - // - // {/* Error Screen */} - // {isErrorView && ( - // <> - // - // - // - // Oops! Something went wrong. An error occurred while - // processing your feedback. - // - // - // {privacyPolicyField} - // - // - // - // - // )} - // - // ); } ) 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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+ +
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+ +
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +exports[`NewsletterSignup Snapshots Renders the bad email UI snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Testing +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+`; + +exports[`NewsletterSignup Snapshots Renders the confirmation UI with confirmationText snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Testing +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+ + + + + + + +

+ You did great! +

+
+
+
+`; + +exports[`NewsletterSignup Snapshots Renders the default confirmation UI snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Testing +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+ + + + + + + +

+ 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. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +exports[`NewsletterSignup Snapshots Renders the error UI snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Testing +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+ + + + + + + +

+ Oops! Something went wrong. +

+
+
+
+`; + +exports[`NewsletterSignup Snapshots Renders the form UI with description snapshot correctly 1`] = ` +
+ +
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
+
+
+`; + +exports[`NewsletterSignup Snapshots Renders the form UI with formHelperText snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Testing +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+ You need help. +
+
+
+
+ +
+
+
+
+
+`; 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} /> - + - - - - )} - - {/* Confirmation Screen */} - {confirmationView && ( - <> - - + + - {confirmationText} - - + + + + + )} - - {/* 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!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

`; -exports[`NewsletterSignup Snapshots Renders the default confirmation UI snapshot correctly 1`] = ` +exports[`NewsletterSignup Snapshots Renders the default confirmation state snapshot correctly 1`] = `
@@ -2072,7 +2072,7 @@ exports[`NewsletterSignup Snapshots Renders the default confirmation UI snapshot

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

`; -exports[`NewsletterSignup Snapshots Renders the error UI snapshot correctly 1`] = ` +exports[`NewsletterSignup Snapshots Renders the error state snapshot correctly 1`] = `
@@ -2357,7 +2357,7 @@ exports[`NewsletterSignup Snapshots Renders the error UI snapshot correctly 1`]

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

- Testing + Sign Up for Our Newsletter!

`; + +exports[`NewsletterSignup Snapshots Renders the submitting state snapshot correctly 1`] = ` +
+
+
+   +
+
+

+ Sign Up for Our Newsletter! +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + + + + + + + + + +
+
+
+ +
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+ +
+
+`; diff --git a/src/helpers/getSectionColors.ts b/src/helpers/getSectionColors.ts index 4365713824..feaf4d00d3 100644 --- a/src/helpers/getSectionColors.ts +++ b/src/helpers/getSectionColors.ts @@ -3,7 +3,8 @@ * Requires a section type as defined by sectionDataMap. Accepts an optional color * preference ("primary" or "secondary"). * - * @returns A string if one color is requested or an object containing both (default). + * @returns An object containing both primary and secondary colors (default) or a string of the value requested + * in the optional color preference. */ import { sectionDataMap, SectionTypes } from "./types"; From f9dc57dc8e6c4c66ea955f44dab94bdfd64c1452 Mon Sep 17 00:00:00 2001 From: Andrew Arnold Date: Thu, 28 Sep 2023 14:52:50 -0400 Subject: [PATCH 14/39] NewsletterSignup Snapshot Update --- .../NewsletterSignup.test.tsx.snap | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap index c4e3ca8ad5..95639f1a9f 100644 --- a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap +++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap @@ -43,9 +43,9 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup Date: Thu, 28 Sep 2023 16:39:57 -0400 Subject: [PATCH 15/39] NewsletterSignup Minor Updates --- .../NewsletterSignup.stories.tsx | 9 ++++++--- .../NewsletterSignup/NewsletterSignup.tsx | 18 +++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 5cc7b807aa..ea432a64da 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -90,6 +90,9 @@ const NewsletterSignupWithControls = (args) => { }, 3000); } console.log("Submitted email: ", userEmail); + setTimeout(() => { + setView("form"); + }, 10000); } return ( @@ -107,7 +110,7 @@ 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."; + "If you use bad@nypl.org you will get the invalid email screen. The form resets a few seconds after confirmation or error."; const confirmationText = "Fantastic! You're all set. Check the console for the data you submitted."; const formHelperText = @@ -120,9 +123,8 @@ const formHelperText = export const WithControls: Story = { args: { className: undefined, - confirmationText, // Shorthand. Value defined above. + confirmationText, // Shorthand. Value defined above in like-named constant. descriptionText, - emailValue: "", formHelperText, id: undefined, isInvalidEmail: false, @@ -130,6 +132,7 @@ export const WithControls: Story = { onChange: undefined, onSubmit: undefined, title, + valueEmail: "", view: "form", }, parameters: { diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index 13c47f477d..4a97e3db21 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -36,29 +36,28 @@ interface NewsletterSignupProps { isInvalidEmail?: boolean; /** Optional: Value to determine the section color highlight */ newsletterSignupType?: SectionTypes; - /** Required: a 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; - /** Required: a handler function that will be called when the text input changes. */ + /** 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. */ + /** Required: The value of the email text input field. */ valueEmail: string; /** 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."; + "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."; + "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. - * - * 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( @@ -180,7 +179,8 @@ export const NewsletterSignup = chakra( name="errorFilled" size="large" /> - Oops! Something went wrong. + Oops! Something went wrong.{" "} + {/* This text is boilerplate and not meant to be customized. */} )} From 117de843e0f46e7e5b06cda4c7d433caf7e46815 Mon Sep 17 00:00:00 2001 From: Andrew Arnold Date: Thu, 28 Sep 2023 16:41:20 -0400 Subject: [PATCH 16/39] NewsletterSignup Snapshot Update --- .../__snapshots__/NewsletterSignup.test.tsx.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap index 95639f1a9f..6419ece481 100644 --- a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap +++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap @@ -2456,6 +2456,7 @@ exports[`NewsletterSignup Snapshots Renders the error state snapshot correctly 1 > Oops! Something went wrong.

+

From 8c603a985cb0d0068edce3a884543905965e63cd Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Wed, 4 Oct 2023 08:45:10 -0400 Subject: [PATCH 17/39] code/storybook clean up --- .../NewsletterSignup/NewsletterSignup.mdx | 94 +- .../NewsletterSignup.test.tsx | 26 +- .../NewsletterSignup/NewsletterSignup.tsx | 63 +- .../NewsletterSignup.test.tsx.snap | 2228 ++++++++--------- src/helpers/getSectionColors.ts | 17 +- src/helpers/types.ts | 116 +- src/theme/components/newsletterSignup.ts | 41 +- 7 files changed, 1169 insertions(+), 1416 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx index 9c0b43b13d..81369cce52 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.mdx +++ b/src/components/NewsletterSignup/NewsletterSignup.mdx @@ -22,7 +22,6 @@ import Link from "../Link/Link"; - **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. @@ -34,53 +33,6 @@ In practice, the consuming app is responsible for handling the form submission. - - ## Accessibility The `NewsletterSignup` component is a complex component built from various Reservoir @@ -124,3 +76,49 @@ Resources: - [DS Radio Accessibility](../?path=/docs/components-form-elements-radio--docs#accessibility) - [DS RadioGroup Accessibility](../?path=/docs/components-form-elements-radiogroup--docs#accessibility) +## 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 returned object will always conatain the "email" field. + +Below is an example callback function named `onSubmit` that is passed to the +`NewsletterSignup` component's `onSubmit` prop and how the view is controlled +in the data submission process. The form data will be returned through +the function's argument as an object, called `values` in the example below. + + { + e.preventDefault(); + setView("submitting"); + const endpoint = "..."; + //Form the request for sending data to the server. + const options = { + method: "POST", + headers: {Content-Type: "application/json"}, + body: JSON.stringify(values), + }; + //Send the form and await response. + try { + const response = await fetch(endpoint, options); + const result = await response.json(); + setView("confirmation"); + } catch (error) { + setView("error"); + } +}; + +//.... + + + +`} +language="jsx" +/> diff --git a/src/components/NewsletterSignup/NewsletterSignup.test.tsx b/src/components/NewsletterSignup/NewsletterSignup.test.tsx index 87fd8d40fb..0c6628c38b 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.test.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.test.tsx @@ -10,9 +10,9 @@ import { sectionDataMap } from "../../helpers/types"; // screen.debug(); describe("NewsletterSignup Accessibility", () => { - const onSubmit = jest.fn(), - onChange = jest.fn(), - valueEmail = ""; + const onSubmit = jest.fn(); + const onChange = jest.fn(); + const valueEmail = ""; it("Form state w/ all optional props (displayed and undisplayed) passes accessibility", async () => { const { container } = render( { * way to test that the component does what it should when the button is clicked. */ - const onSubmit = jest.fn(), - onChange = jest.fn(), - valueEmail = ""; + const onSubmit = jest.fn(); + const onChange = jest.fn(); + const valueEmail = ""; it("Renders the Minimum Required Elements for the Form", () => { render( { }); // Close unit tests. describe("NewsletterSignup Snapshots", () => { - const onSubmit = jest.fn(), - onChange = jest.fn(), - valueEmail = ""; + const onSubmit = jest.fn(); + const onChange = jest.fn(); + const valueEmail = ""; it("Renders the default form UI snapshot correctly", () => { const view = renderer .create( @@ -307,12 +307,12 @@ describe("NewsletterSignup Snapshots", () => { describe("Renders each color for each newsletterSignupType correctly", () => { // The newsletterSignupType values are determined by the types contained in the sectionDataMap. // So it is safe to use the map directly. - sectionDataMap.map((section) => { + Object.keys(sectionDataMap).map((section) => { it( "Renders " + - section.type + + section + " color band with " + - section.colorVals.primary, + sectionDataMap[section].primary, () => { const view = renderer .create( @@ -320,7 +320,7 @@ describe("NewsletterSignup Snapshots", () => { onSubmit={onSubmit} onChange={onChange} valueEmail={valueEmail} - newsletterSignupType={section.type} + newsletterSignupType={section} /> ) .toJSON(); diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index 4a97e3db21..a99d7ea2d4 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -16,7 +16,6 @@ import Text from "../Text/Text"; import TextInput from "../TextInput/TextInput"; import Heading from "../Heading/Heading"; import useNYPLBreakpoints from "../../hooks/useNYPLBreakpoints"; -import { getSectionColors } from "../../helpers/getSectionColors"; import { SectionTypes } from "../../helpers/types"; interface NewsletterSignupProps { @@ -42,8 +41,8 @@ interface NewsletterSignupProps { 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; + /** Optional: The value of the email text input field. */ + valueEmail?: string; /** Optional: Used to specify what is displayed in the component form/feedback area. */ view?: "form" | "submitting" | "confirmation" | "error"; } @@ -60,7 +59,7 @@ const defaultDescriptionText = * email-based newsletter distribution list. */ export const NewsletterSignup = chakra( - forwardRef( + forwardRef( ( { className, @@ -71,7 +70,7 @@ export const NewsletterSignup = chakra( isInvalidEmail = false, newsletterSignupType = "whatsOn", onChange, - valueEmail = "", + valueEmail, onSubmit, title = "Sign Up for Our Newsletter!", view = "form", @@ -80,7 +79,9 @@ export const NewsletterSignup = chakra( ref? ) => { const { isLargerThanMobile } = useNYPLBreakpoints(); - const styles = useStyleConfig("NewsletterSignup", {}); + const styles = useStyleConfig("NewsletterSignup", { + newsletterSignupType, + }); const iconColor = useColorModeValue(null, "dark.ui.typography.body"); const isFormView = view === "form" || view === "submitting"; @@ -99,47 +100,39 @@ export const NewsletterSignup = chakra( __css={styles} {...rest} > - - + + {descriptionText} + -   - - - - {descriptionText} - - Privacy Policy - - - - + Privacy Policy + + + {isFormView && (
- + - + -

+ Thank you for signing up! +

- +

+ You can update your email subscription preferences at any time using the links at the bottom of the email. +

+
`; -exports[`NewsletterSignup Snapshots Renders the error state snapshot correctly 1`] = ` +exports[`NewsletterSignup Snapshots Renders the default error state snapshot correctly 1`] = ` +`; + +exports[`NewsletterSignup Snapshots Renders the default form UI snapshot correctly 1`] = ` +
+
+

+ Sign Up for Our Newsletter! +

+

+ Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections. +

+ + Privacy Policy + + This link opens in a new window + -

+

+
+
+
- Oops! Something went wrong. -

-
+
+
+ +
+ +
+
+ Ex: + jdoe@domain.com +
+
+
+
+
+ +
+
+
`; @@ -2308,23 +2387,21 @@ exports[`NewsletterSignup Snapshots Renders the form UI with description snapsho className="chakra-stack css-j7qwjs" >
({ alignItems: "center", borderWidth: { base: "0px 1px 1px 1px", md: "1px 1px 1px 0px" }, maxWidth: "1280px", width: "100%", - "div#pitch": { + pitch: { // It's a two-column layout >md width: { base: "100%", md: "50%" }, bg: "ui.bg.default", @@ -39,22 +40,21 @@ const NewsLetterSignup = { }, }, padding: { - 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. + base: "var(--nypl-space-s) var(--nypl-space-l) var(--nypl-space-l) var(--nypl-space-l)", md: "l", lg: "var(--nypl-space-l) var(--nypl-space-xxl) var(--nypl-space-l) var(--nypl-space-xl)", }, alignItems: "flex-start", gap: "xs", + margin: "unset", }, - "div#pitch, h3, #pitch>p, #pitch>a": { margin: "unset" }, - "a#privacy": { - display: "flex", - alignItems: "center", + privacy: { gap: "xxs", fontSize: "desktop.caption", fontWeight: "caption", + margin: "unset", }, - "div#action": { + action: { padding: { base: "l", lg: "var(--nypl-space-l) var(--nypl-space-xxl)" }, // It's a two-column layout >md width: { base: "100%", md: "50%" }, @@ -62,6 +62,7 @@ const NewsLetterSignup = { form: { width: "100%", }, + // Overwrites the defaut styling of the From component layout "#newsletter-form-parent": { // The button is 78px wide and must sit to the right of the input field >lg. gridTemplateColumns: { base: null, xl: "1fr 78px" }, From 9b31ea7da6bee9fdac05c87524e2db6b7c64af3a Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Wed, 11 Oct 2023 11:41:42 -0400 Subject: [PATCH 29/39] update storybook stories, fix typo --- .../NewsletterSignup/NewsletterSignup.mdx | 19 ++++---- .../NewsletterSignup.stories.tsx | 45 ++----------------- 2 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx index 1f5c604000..c2ccca82b6 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.mdx +++ b/src/components/NewsletterSignup/NewsletterSignup.mdx @@ -22,17 +22,9 @@ import Link from "../Link/Link"; -**Note**: The `NewsletterSignup` example below will render different states depending on what email address you input. -* `error@nypl.org` will display the error screen. -* `bad@nypl.org` will display the bad email screen. -* Any other validly-constructed email address will display the confirmation screen after three seconds to better -demonstrate the `submitting` state. - -The value of the email will appear in the console log. - ## Component Props - + @@ -45,6 +37,9 @@ Within the `NewsletterSignup` component, the DS `form` component wraps around tw Those`FormField` components hold a DS `TextInput` component of `type="email"` and a DS `Button` component of `type="submit"` respectively. 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. + Resources: - [DS Form Accessibility](../?path=/docs/components-form-elements-form--docs#accessibility) @@ -56,7 +51,9 @@ Resources: Alternatively to a `descriptionText` of type `string`, a HTML Element or React component can be passed. When passing a JSX Element, the consuming app is responsible to assure its accessibility. - +_NOTE: This is applicable for all component props excepting HTML/JSX elements._ + + ## Form Submission Data @@ -107,4 +104,4 @@ language="jsx" ## Component States - + diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 99e2f72926..a9b5937b57 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -4,7 +4,6 @@ import { withDesign } from "storybook-addon-designs"; import NewsletterSignup from "./NewsletterSignup"; import { sectionTypeArray } from "../../helpers/types"; import Heading from "../Heading/Heading"; -import useStateWithDependencies from "../../hooks/useStateWithDependencies"; const meta: Meta = { title: "Components/Form Elements/NewsletterSignup", @@ -81,44 +80,6 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const NewsletterSignupWithControls = (args) => { - const [view, setView] = useStateWithDependencies(args.view); - const [isInvalidEmail, setIsInvalidEmail] = useStateWithDependencies( - args.isInvalidEmail - ); - - function handleSubmit(event): void { - event.preventDefault(); - setView("submitting"); - 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); - setTimeout(() => { - setView("form"); - }, 10000); - } - return ( - - ); -}; /** * Main Story for the NewsletterSignup component. This must contains the `args` * and `parameters` properties in this object. @@ -130,6 +91,8 @@ export const WithControls: Story = { confirmationText: "You can update your email subscription preferences at any time using the links at the bottom of the email.", descriptionText: undefined, + errorHeading: undefined, + errorText: undefined, formHelperText: undefined, id: undefined, isInvalidEmail: false, @@ -147,7 +110,7 @@ export const WithControls: Story = { }, jest: "NewsletterSignup.test.tsx", }, - render: (args) => , + render: (args) => , }; export const DescriptionUsingJSXElements: Story = { @@ -214,7 +177,7 @@ export const ComponentStates: Story = { - Deafult Error View + Default Error View Date: Wed, 11 Oct 2023 14:16:01 -0400 Subject: [PATCH 30/39] add interavtive example to storybook --- .../NewsletterSignup/NewsletterSignup.mdx | 45 ++++++++++++++++ .../NewsletterSignup.stories.tsx | 54 ++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx index c2ccca82b6..2342707395 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.mdx +++ b/src/components/NewsletterSignup/NewsletterSignup.mdx @@ -102,6 +102,51 @@ const onSubmit = async (values) => { language="jsx" /> +## Interactive Example with onChange and onSubmit + +_NOTE: open the browser console to see the values logged in the example below._ + +The input value typed into the `TextInput` of the `NewsletterSignup` component can be accessed by the +functions passed to the `onChange` and `onSubmit` prop. + +Both the `onChange` and `onSubmit` callback functions can retrieved the submitted value as `event.target.email.value` +through the `event` object passed as the single argument. + +The following example logs the `event.target.email.value` to the console on each `onChange` call and upon +cklicking the Submit button which triggers the `onSubmit` function and simulate a submission. +The component will transition through a `"submitting"` view to an alternating `"confirmation"`, `"error"` or "invalid Email" view. + + { + console.log(\`onChange Email Input value: \${event.target.value}\`); + setInputVal(event.target.value); + }; + const handleSubmit = (event) => { + event.preventDefault(); + console.log(\`onSubmit Email Input value: \${event.target.email.value}\`); + }; + return ( + + ); +} `} + language="jsx" +/> + + + ## Component States diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index a9b5937b57..1a24bb9fe1 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -1,7 +1,8 @@ +import * as React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { Box, VStack } from "@chakra-ui/react"; import { withDesign } from "storybook-addon-designs"; -import NewsletterSignup from "./NewsletterSignup"; +import NewsletterSignup, { NewsletterSignupViewType } from "./NewsletterSignup"; import { sectionTypeArray } from "../../helpers/types"; import Heading from "../Heading/Heading"; @@ -112,6 +113,57 @@ export const WithControls: Story = { }, render: (args) => , }; +let counter = 0; + +function NewsletterSignupOnSubmitExampleComponent() { + const [view, setView]: [ + NewsletterSignupViewType, + React.Dispatch> + ] = React.useState("form"); + const [inputVal, setInputVal] = React.useState(""); + + const changeView = () => { + counter++; + setView(counter === 1 ? "confirmation" : counter === 2 ? "error" : "form"); + setTimeout(() => { + setView("form"); + setInputVal(""); + }, 2000); + }; + + React.useEffect(() => { + if (counter === 3) counter = 0; + }, [counter]); + + const handleChange = (event) => { + console.log(`onChange Email Input value: ${event.target.value}`); + setInputVal(event.target.value); + }; + const handleSubmit = (event) => { + event.preventDefault(); + setView("submitting"); + console.log(`onSubmit Email Input value: ${event.target.email.value}`); + setTimeout(changeView, 2000); + }; + + return ( + + ); +} + +export const NewsletterSignupOnSubmitExample: Story = { + render: () => , + name: "Interactive Example", +}; export const DescriptionUsingJSXElements: Story = { render: () => ( From cfcab79f58e472c3398937764d06cdc9d8941eb1 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Wed, 11 Oct 2023 14:53:41 -0400 Subject: [PATCH 31/39] add actions for onChange and onSubmit --- .../NewsletterSignup/NewsletterSignup.stories.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 1a24bb9fe1..6fbd9bd253 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { Box, VStack } from "@chakra-ui/react"; +import { action } from "@storybook/addon-actions"; import { withDesign } from "storybook-addon-designs"; import NewsletterSignup, { NewsletterSignupViewType } from "./NewsletterSignup"; import { sectionTypeArray } from "../../helpers/types"; @@ -55,8 +56,8 @@ const meta: Meta = { }, }, }, - onChange: { control: false }, - onSubmit: { control: false }, + onChange: { control: false, action: "onChange" }, + onSubmit: { control: false, action: "onSubmit" }, title: { control: "text", table: { @@ -98,8 +99,13 @@ export const WithControls: Story = { id: undefined, isInvalidEmail: false, newsletterSignupType: undefined, - onChange: undefined, - onSubmit: undefined, + onChange: (event) => { + action("onChange")(event.target.value); + }, + onSubmit: (event) => { + event.preventDefault(); + action("onSubmit")(event.target[0].value); + }, title: undefined, valueEmail: undefined, view: undefined, From 8e5430fb1353f1bfa28f06cb9f207c4903600e32 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 05:58:57 -0400 Subject: [PATCH 32/39] remove styling, fix typos --- src/components/NewsletterSignup/NewsletterSignup.mdx | 4 ++-- src/theme/components/newsletterSignup.ts | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx index 2342707395..0af665bcb2 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.mdx +++ b/src/components/NewsletterSignup/NewsletterSignup.mdx @@ -51,7 +51,7 @@ Resources: Alternatively to a `descriptionText` of type `string`, a HTML Element or React component can be passed. When passing a JSX Element, the consuming app is responsible to assure its accessibility. -_NOTE: This is applicable for all component props excepting HTML/JSX elements._ +_NOTE: This is applicable for all component props accepting HTML/JSX elements._ @@ -113,7 +113,7 @@ Both the `onChange` and `onSubmit` callback functions can retrieved the submitte through the `event` object passed as the single argument. The following example logs the `event.target.email.value` to the console on each `onChange` call and upon -cklicking the Submit button which triggers the `onSubmit` function and simulate a submission. +clicking the Submit button which triggers the `onSubmit` function and simulate a submission. The component will transition through a `"submitting"` view to an alternating `"confirmation"`, `"error"` or "invalid Email" view. ({ alignItems: "center", @@ -46,13 +46,10 @@ const NewsLetterSignup = { }, alignItems: "flex-start", gap: "xs", - margin: "unset", }, privacy: { - gap: "xxs", fontSize: "desktop.caption", fontWeight: "caption", - margin: "unset", }, action: { padding: { base: "l", lg: "var(--nypl-space-l) var(--nypl-space-xxl)" }, @@ -75,4 +72,4 @@ const NewsLetterSignup = { }), }; -export default NewsLetterSignup; +export default NewsletterSignup; From d34bcc77e87b4b8db4254a27fc6b03bfeb9c4b83 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 06:16:20 -0400 Subject: [PATCH 33/39] update as per PR comments --- .../NewsletterSignup.stories.tsx | 24 +++++++++++++++++++ .../NewsletterSignup/NewsletterSignup.tsx | 10 ++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 6fbd9bd253..ca6ae1158a 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -58,6 +58,15 @@ const meta: Meta = { }, onChange: { control: false, action: "onChange" }, onSubmit: { control: false, action: "onSubmit" }, + privacyPolicyLink: { + control: "text", + table: { + defaultValue: { + summery: + "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", + }, + }, + }, title: { control: "text", table: { @@ -106,6 +115,8 @@ export const WithControls: Story = { event.preventDefault(); action("onSubmit")(event.target[0].value); }, + privacyPolicyLink: + "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", title: undefined, valueEmail: undefined, view: undefined, @@ -119,6 +130,7 @@ export const WithControls: Story = { }, render: (args) => , }; +/* Counter to allow the interactive example to show different states uponn submit*/ let counter = 0; function NewsletterSignupOnSubmitExampleComponent() { @@ -272,3 +284,15 @@ export const ComponentStates: Story = { ), }; + +/* To fix focus issue where the page focuses on the last NewsletterSignup +component example */ +const setFocus = () => { + const heading = document.getElementById( + "anchor--components-form-elements-newslettersignup--with-controls" + ); + heading.focus(); + heading.scrollIntoView({ behavior: "smooth" }); +}; + +setTimeout(setFocus, 1000); diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index 6753c795b3..b447df61bc 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -81,7 +81,7 @@ export const NewsletterSignup = chakra( formHelperText, id, isInvalidEmail = false, - newsletterSignupType = "whatsOn", + newsletterSignupType = "blogs", onChange, onSubmit, privacyPolicyLink = "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", @@ -114,10 +114,10 @@ export const NewsletterSignup = chakra( {...rest} > - {title && } + {title && } {descriptionText ? ( typeof descriptionText === "string" ? ( - {descriptionText} + {descriptionText} ) : ( descriptionText ) @@ -126,7 +126,8 @@ export const NewsletterSignup = chakra( Privacy Policy @@ -208,7 +209,6 @@ export const NewsletterSignup = chakra( name="errorFilled" size="large" /> - {/* This text is boilerplate and not meant to be customized. */} Date: Thu, 12 Oct 2023 06:23:50 -0400 Subject: [PATCH 34/39] update snapshots --- .../NewsletterSignup.test.tsx.snap | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap index 4d91c0984d..3e177b234b 100644 --- a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap +++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap @@ -8,17 +8,17 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup className="chakra-stack css-p38jk0" >

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Why not?

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Sign Up for Our Newsletter!

Stay connected with the latest research news from NYPL, including information about our events, programs, exhibitions, and collections.

Date: Thu, 12 Oct 2023 11:17:59 -0400 Subject: [PATCH 35/39] update props, comments as discussed, update test and doc accordingly --- .../NewsletterSignup/NewsletterSignup.mdx | 11 ++ .../NewsletterSignup.stories.tsx | 24 +++- .../NewsletterSignup.test.tsx | 8 +- .../NewsletterSignup/NewsletterSignup.tsx | 17 ++- .../NewsletterSignup.test.tsx.snap | 120 +++++++++--------- .../newsletterSignupChangelogData.ts | 26 ++++ 6 files changed, 125 insertions(+), 81 deletions(-) create mode 100644 src/components/NewsletterSignup/newsletterSignupChangelogData.ts diff --git a/src/components/NewsletterSignup/NewsletterSignup.mdx b/src/components/NewsletterSignup/NewsletterSignup.mdx index 0af665bcb2..f7d5ce9343 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.mdx +++ b/src/components/NewsletterSignup/NewsletterSignup.mdx @@ -2,6 +2,8 @@ import { ArgTypes, Canvas, Description, Meta, Source } from "@storybook/blocks"; import * as NewsletterSignupStories from "./NewsletterSignup.stories"; import Link from "../Link/Link"; +import ComponentChangelogTable from "../../utils/ComponentChangelogTable"; +import { changelogData } from "./newsletterSignupChangelogData"; @@ -17,6 +19,7 @@ import Link from "../Link/Link"; - {Overview} - {Component Props} - {Accessibility} +- {Changelog} ## Overview @@ -33,6 +36,9 @@ import Link from "../Link/Link"; The `NewsletterSignup` component is a complex component built from various Reservoir DS and Chakra components. +The `title` prop of the `NewsletterSignup` component expects a `HTML Element` or a `React Component`. By default it renderes a `h2` tag but it is +the responsibility of the consuming app to pass the heading tag (`h*`) that aligns with the page structure and ensures accessibility. + Within the `NewsletterSignup` component, the DS `form` component wraps around two DS `FormField` components. Those`FormField` components hold a DS `TextInput` component of `type="email"` and a DS `Button` component of `type="submit"` respectively. Each of these components has their own accessibility features documented in their respective Storybook pages. @@ -42,6 +48,7 @@ error message if an error occurs. Resources: +- [W3C WAI Headings](https://www.w3.org/WAI/tutorials/page-structure/headings/) - [DS Form Accessibility](../?path=/docs/components-form-elements-form--docs#accessibility) - [DS Button Accessibility](../?path=/docs/components-form-elements-button--docs#accessibility) - [DS TextInput Accessibility](../?path=/docs/components-form-elements-textinput--docs#accessibility) @@ -150,3 +157,7 @@ function NewsletterSignupOnSubmitExampleComponent() { ## Component States + +## Changelog + + diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index ca6ae1158a..1d1d5cbca8 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -68,10 +68,17 @@ const meta: Meta = { }, }, title: { - control: "text", + control: false, + mapping: { + default: ( + + ), + }, table: { + control: "text", defaultValue: { - summary: "Sign Up for Our Newsletter!", + summary: + '', }, }, }, @@ -115,9 +122,10 @@ export const WithControls: Story = { event.preventDefault(); action("onSubmit")(event.target[0].value); }, - privacyPolicyLink: - "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", - title: undefined, + privacyPolicyLink: undefined, + title: ( + + ), valueEmail: undefined, view: undefined, }, @@ -286,7 +294,9 @@ export const ComponentStates: Story = { }; /* To fix focus issue where the page focuses on the last NewsletterSignup -component example */ +component example. +Note: This behavior only effects the storybook doc and is caused by rendering +a list of the component in different states. This issue should not happen on a consuming app page*/ const setFocus = () => { const heading = document.getElementById( "anchor--components-form-elements-newslettersignup--with-controls" @@ -295,4 +305,4 @@ const setFocus = () => { heading.scrollIntoView({ behavior: "smooth" }); }; -setTimeout(setFocus, 1000); +setTimeout(setFocus, 2000); diff --git a/src/components/NewsletterSignup/NewsletterSignup.test.tsx b/src/components/NewsletterSignup/NewsletterSignup.test.tsx index aeeed6ed51..e6fadf85bb 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.test.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.test.tsx @@ -18,7 +18,6 @@ describe("NewsletterSignup Accessibility", () => { { { { { { 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.getByRole("heading", { level: 2 })).toBeInTheDocument(); expect(screen.getByText(/Privacy Policy/i)).toBeInTheDocument(); }); diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index b447df61bc..08dde0d799 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -38,7 +38,9 @@ interface NewsletterSignupProps { id?: string; /** Toggles the invalid state for the email field. */ isInvalidEmail?: boolean; - /** Value to determine the section color highlight */ + /** Value to determine the section color highlight. The default is set to "blogs" as it uses the + * "ui.border.deafult" color. + */ newsletterSignupType?: SectionTypes; /** A handler function that will be called when the form is submitted. */ onSubmit: (event: React.FormEvent) => void; @@ -46,8 +48,8 @@ interface NewsletterSignupProps { onChange: (event: React.ChangeEvent) => void; /** Link to the relevant privacy policy page. */ privacyPolicyLink?: string; - /** Used to populate the `

` header title. */ - title?: string; + /** Used to populate the title of the Component*/ + title?: JSX.Element; /** The value of the email text input field. */ valueEmail?: string; /** Used to specify what is displayed in the component form/feedback area. */ @@ -86,7 +88,9 @@ export const NewsletterSignup = chakra( onSubmit, privacyPolicyLink = "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", valueEmail, - title = "Sign Up for Our Newsletter!", + title = ( + + ), view = "form", ...rest }, @@ -114,10 +118,10 @@ export const NewsletterSignup = chakra( {...rest} > - {title && } + {title} {descriptionText ? ( typeof descriptionText === "string" ? ( - {descriptionText} + {descriptionText} ) : ( descriptionText ) @@ -126,7 +130,6 @@ export const NewsletterSignup = chakra( diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap index 3e177b234b..76673b2198 100644 --- a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap +++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap @@ -7,11 +7,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup
-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -153,11 +153,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -299,11 +299,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -445,11 +445,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -591,11 +591,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -737,11 +737,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -883,11 +883,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1029,11 +1029,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1175,11 +1175,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1321,11 +1321,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1467,11 +1467,11 @@ exports[`NewsletterSignup Snapshots Renders each color for each newsletterSignup

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1613,11 +1613,11 @@ exports[`NewsletterSignup Snapshots Renders the bad email UI snapshot correctly

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1765,11 +1765,11 @@ exports[`NewsletterSignup Snapshots Renders the confirmation UI with confirmatio

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -1885,11 +1885,11 @@ exports[`NewsletterSignup Snapshots Renders the custom error state snapshot corr

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2008,11 +2008,11 @@ exports[`NewsletterSignup Snapshots Renders the default confirmation state snaps

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2128,11 +2128,11 @@ exports[`NewsletterSignup Snapshots Renders the default error state snapshot cor

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2243,11 +2243,11 @@ exports[`NewsletterSignup Snapshots Renders the default form UI snapshot correct

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2389,11 +2389,11 @@ exports[`NewsletterSignup Snapshots Renders the form UI with description snapsho

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2535,11 +2535,11 @@ exports[`NewsletterSignup Snapshots Renders the form UI with formHelperText snap

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

@@ -2682,11 +2682,11 @@ exports[`NewsletterSignup Snapshots Renders the submitting state snapshot correc

-

- Sign Up for Our Newsletter! -

+ Sign Up for Our Newsletter +

diff --git a/src/components/NewsletterSignup/newsletterSignupChangelogData.ts b/src/components/NewsletterSignup/newsletterSignupChangelogData.ts new file mode 100644 index 0000000000..43ad669a08 --- /dev/null +++ b/src/components/NewsletterSignup/newsletterSignupChangelogData.ts @@ -0,0 +1,26 @@ +/** This data is used to populate the ComponentChangelogTable component. + * + * date: string (when adding new entry during development, set value as "Prerelease") + * version: string (when adding new entry during development, set value as "Prerelease") + * type: "Bug Fix" | "New Feature" | "Update"; + * affects: array["Accessibility" | "Documentation" | "Functionality" | "Styles"]; + * notes: array (will render as a bulleted list, add one array element for each list element) + */ +import { ChangelogData } from "../../utils/ComponentChangelogTable"; + +export const changelogData: ChangelogData[] = [ + { + date: "Prerelease", + version: "Prerelease", + type: "New Feature", + affects: [], + notes: ["Adds the NewsletterSignup component to the DS library"], + }, + { + date: "Prerelease", + version: "Prerelease", + type: "New Feature", + affects: [], + notes: ["Adds the NewsletterSignup component to the DS library"], + }, +]; From 3a56d1fb35ea0b1bac2d3e24de669188e5004540 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 12:45:43 -0400 Subject: [PATCH 36/39] update breakpoints, remove reduntant content --- .../NewsletterSignup/newsletterSignupChangelogData.ts | 7 ------- src/theme/components/newsletterSignup.ts | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/components/NewsletterSignup/newsletterSignupChangelogData.ts b/src/components/NewsletterSignup/newsletterSignupChangelogData.ts index 43ad669a08..23903de36c 100644 --- a/src/components/NewsletterSignup/newsletterSignupChangelogData.ts +++ b/src/components/NewsletterSignup/newsletterSignupChangelogData.ts @@ -16,11 +16,4 @@ export const changelogData: ChangelogData[] = [ affects: [], notes: ["Adds the NewsletterSignup component to the DS library"], }, - { - date: "Prerelease", - version: "Prerelease", - type: "New Feature", - affects: [], - notes: ["Adds the NewsletterSignup component to the DS library"], - }, ]; diff --git a/src/theme/components/newsletterSignup.ts b/src/theme/components/newsletterSignup.ts index 4740be82fe..dc40bc888d 100644 --- a/src/theme/components/newsletterSignup.ts +++ b/src/theme/components/newsletterSignup.ts @@ -62,12 +62,12 @@ const NewsletterSignup = { // Overwrites the defaut styling of the From component layout "#newsletter-form-parent": { // The button is 78px wide and must sit to the right of the input field >lg. - gridTemplateColumns: { base: null, xl: "1fr 78px" }, + gridTemplateColumns: { base: null, lg: "1fr 78px" }, gap: { base: "s", xl: "xs" }, }, button: { // The button must align w/ the input field, but using {align-items: center} doesn't quite work due to the input field not being the literal v-center. - marginTop: { base: null, xl: "31px" }, + marginTop: { base: null, lg: "31px" }, }, }), }; From 917feb017d56eb411853061cf04082481b04eeab Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 12:50:29 -0400 Subject: [PATCH 37/39] update breakpoint - the one that got away --- src/theme/components/newsletterSignup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/components/newsletterSignup.ts b/src/theme/components/newsletterSignup.ts index dc40bc888d..00b0b3f019 100644 --- a/src/theme/components/newsletterSignup.ts +++ b/src/theme/components/newsletterSignup.ts @@ -63,7 +63,7 @@ const NewsletterSignup = { "#newsletter-form-parent": { // The button is 78px wide and must sit to the right of the input field >lg. gridTemplateColumns: { base: null, lg: "1fr 78px" }, - gap: { base: "s", xl: "xs" }, + gap: { base: "s", lg: "xs" }, }, button: { // The button must align w/ the input field, but using {align-items: center} doesn't quite work due to the input field not being the literal v-center. From 3f42b6afa57347e0be0ff1d5eff7a10583c4bda1 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 13:09:10 -0400 Subject: [PATCH 38/39] change html tags to ds components --- .../NewsletterSignup.stories.tsx | 18 ++++++++++-------- .../NewsletterSignup/NewsletterSignup.tsx | 12 +++++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 1d1d5cbca8..93f4ab8cd8 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -6,6 +6,8 @@ import { withDesign } from "storybook-addon-designs"; import NewsletterSignup, { NewsletterSignupViewType } from "./NewsletterSignup"; import { sectionTypeArray } from "../../helpers/types"; import Heading from "../Heading/Heading"; +import Link from "../Link/Link"; +import Text from "../Text/Text"; const meta: Meta = { title: "Components/Form Elements/NewsletterSignup", @@ -199,10 +201,10 @@ export const DescriptionUsingJSXElements: Story = { onChange={() => {}} onSubmit={() => {}} descriptionText={ -

+ If the description text needs to have special styling or needs - to have a nested link -
+ to have a nested link + } confirmationHeading="Thank you for signing up!" confirmationText="You can update your email subscription preferences at any time using the links at the bottom of the email." @@ -279,13 +281,13 @@ export const ComponentStates: Story = { confirmationText="You can update your email subscription preferences at any time using the links at the bottom of the email." errorHeading="An error has occurred." errorText={ -
- Please refresh this page and try again. If this error persists, - + + Please refresh this page and try again. If this error persists,{" "} + contact our e-mail team - + . -
+ } /> diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index 08dde0d799..a95c0cf972 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -121,7 +121,9 @@ export const NewsletterSignup = chakra( {title} {descriptionText ? ( typeof descriptionText === "string" ? ( - {descriptionText} + + {descriptionText} + ) : ( descriptionText ) @@ -192,7 +194,9 @@ export const NewsletterSignup = chakra( {confirmationHeading} - {confirmationText} + + {confirmationText} + )} {view === "error" && ( @@ -224,7 +228,9 @@ export const NewsletterSignup = chakra( {errorText ? ( typeof errorText === "string" ? ( - {errorText} + + {errorText} + ) : ( errorText ) From 77c31d75dac36447ba69ee87e3877199045197e2 Mon Sep 17 00:00:00 2001 From: Isa Stettler Date: Thu, 12 Oct 2023 15:33:56 -0400 Subject: [PATCH 39/39] update Text styles, fix typo for privacyPolicyLink, update tests --- .../NewsletterSignup/NewsletterSignup.stories.tsx | 2 +- src/components/NewsletterSignup/NewsletterSignup.tsx | 6 +++--- .../__snapshots__/NewsletterSignup.test.tsx.snap | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx index 93f4ab8cd8..de392c4a5f 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.stories.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.stories.tsx @@ -64,7 +64,7 @@ const meta: Meta = { control: "text", table: { defaultValue: { - summery: + summary: "https://www.nypl.org/help/about-nypl/legal-notices/privacy-policy", }, }, diff --git a/src/components/NewsletterSignup/NewsletterSignup.tsx b/src/components/NewsletterSignup/NewsletterSignup.tsx index a95c0cf972..656ae7ccd8 100644 --- a/src/components/NewsletterSignup/NewsletterSignup.tsx +++ b/src/components/NewsletterSignup/NewsletterSignup.tsx @@ -186,7 +186,7 @@ export const NewsletterSignup = chakra( size="large" /> {errorText ? ( typeof errorText === "string" ? ( - + {errorText} ) : ( diff --git a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap index 76673b2198..4573acc94c 100644 --- a/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap +++ b/src/components/NewsletterSignup/__snapshots__/NewsletterSignup.test.tsx.snap @@ -1863,7 +1863,7 @@ exports[`NewsletterSignup Snapshots Renders the confirmation UI with confirmatio

Thank you for signing up!

@@ -1983,7 +1983,7 @@ exports[`NewsletterSignup Snapshots Renders the custom error state snapshot corr

Oh no! Something went wrong.

@@ -2106,7 +2106,7 @@ exports[`NewsletterSignup Snapshots Renders the default confirmation state snaps

Thank you for signing up!

@@ -2226,7 +2226,7 @@ exports[`NewsletterSignup Snapshots Renders the default error state snapshot cor

Oops! Something went wrong.