From 1298252bf5eab936a7067e72c5794fd55b15a7e6 Mon Sep 17 00:00:00 2001 From: ssingh Date: Mon, 30 Oct 2023 13:51:29 -0700 Subject: [PATCH] update naming, clean up memo code --- .changeset/nasty-cups-trade.md | 2 +- documentation/specs/SearchNav.md | 2 +- easy-ui-react/src/SearchNav/CTAGroup.tsx | 8 ++ .../src/SearchNav/CondensedSearchNav.tsx | 4 +- .../src/SearchNav/EmphasizedText.tsx | 17 --- easy-ui-react/src/SearchNav/LogoGroup.tsx | 14 ++- easy-ui-react/src/SearchNav/SearchNav.mdx | 6 +- .../src/SearchNav/SearchNav.stories.tsx | 18 +-- .../src/SearchNav/SearchNav.test.tsx | 6 +- easy-ui-react/src/SearchNav/SearchNav.tsx | 119 +++++------------- easy-ui-react/src/SearchNav/Title.module.scss | 3 + easy-ui-react/src/SearchNav/Title.tsx | 24 ++++ easy-ui-react/src/SearchNav/context.ts | 2 +- easy-ui-react/src/SearchNav/utilities.ts | 100 +++++++++++++++ 14 files changed, 195 insertions(+), 130 deletions(-) delete mode 100644 easy-ui-react/src/SearchNav/EmphasizedText.tsx create mode 100644 easy-ui-react/src/SearchNav/Title.module.scss create mode 100644 easy-ui-react/src/SearchNav/Title.tsx create mode 100644 easy-ui-react/src/SearchNav/utilities.ts diff --git a/.changeset/nasty-cups-trade.md b/.changeset/nasty-cups-trade.md index 5ed27e9ad..d11dcb067 100644 --- a/.changeset/nasty-cups-trade.md +++ b/.changeset/nasty-cups-trade.md @@ -2,4 +2,4 @@ "@easypost/easy-ui": minor --- -feat(SearchNav): support PrimaryCTAItem and EmphasizedText components +feat(SearchNav): support PrimaryCTAItem and Title components diff --git a/documentation/specs/SearchNav.md b/documentation/specs/SearchNav.md index 64b53bf46..86e25926e 100644 --- a/documentation/specs/SearchNav.md +++ b/documentation/specs/SearchNav.md @@ -19,7 +19,7 @@ A `SearchNav` is a navigation bar focused on handling dense information interact `SearchNav` will be made up of sub-component containers. At the top level, the `SearchNav` serves as the container for the logo, dropdown, search input, and CTAs. The logo and dropdown will be grouped into a `SearchNav.LogoGroup` container. The search input will be wrapped by a `SearchNav.Search` container. The CTAs will be wrapped by a `SearchNav.CTAGroup` container. -`SearchNav.LogoGroup` will be comprised of `SearchNav.Logo`, a minimal wrapper for the consumer provided logo, `SearchNav.EmphasizedText`, and `SearchNav.Selector`. `SearchNav.Selector` will be built using React Aria's `useSelect`, `useListBox`, `usePopover`, `useOption` and `HiddenSelect`. To help manage state, it will also rely on React Stately's `useSelectState`. +`SearchNav.LogoGroup` will be comprised of `SearchNav.Logo`, a minimal wrapper for the consumer provided logo, `SearchNav.Title`, and `SearchNav.Selector`. `SearchNav.Selector` will be built using React Aria's `useSelect`, `useListBox`, `usePopover`, `useOption` and `HiddenSelect`. To help manage state, it will also rely on React Stately's `useSelectState`. `SearchNav.CTAGroup` will render a primary CTA, `SearchNav.PrimaryCTAItem`, and a secondary CTA, `SearchNav.SecondaryCTAItem`; both will make use of Easy UI's `UnstyledButton` component. diff --git a/easy-ui-react/src/SearchNav/CTAGroup.tsx b/easy-ui-react/src/SearchNav/CTAGroup.tsx index 07e7781ac..44d940730 100644 --- a/easy-ui-react/src/SearchNav/CTAGroup.tsx +++ b/easy-ui-react/src/SearchNav/CTAGroup.tsx @@ -19,6 +19,14 @@ export type CTAGroupProps = { children: ReactNode; }; +/** + * + * @privateRemarks + * This component doesn't directly use children and instead + * reads the nodes it renders from context. This is so we can + * efficiently share the same data across various configurations. + * + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export function CTAGroup(_props: CTAGroupProps) { const { menuOverlayProps, ctaMenuSymbol, primaryCTAItem, secondaryCTAItems } = diff --git a/easy-ui-react/src/SearchNav/CondensedSearchNav.tsx b/easy-ui-react/src/SearchNav/CondensedSearchNav.tsx index a4df594e4..0d537ed5d 100644 --- a/easy-ui-react/src/SearchNav/CondensedSearchNav.tsx +++ b/easy-ui-react/src/SearchNav/CondensedSearchNav.tsx @@ -16,8 +16,8 @@ import { getFlattenedKey } from "../utilities/react"; /** * @privateRemarks * Renders a left aligned menu button and right aligned search button. - * The menu options come from `SearchNav.Selector` and `SearchNav.CTAGroup`. - * On small screens, this effectively replaces `SearchNav`. + * The menu options come from `` and ``. + * On small screens, this effectively replaces ``. */ export function CondensedSearchNav() { const [isMenuOpen, setIsMenuOpen] = useState(false); diff --git a/easy-ui-react/src/SearchNav/EmphasizedText.tsx b/easy-ui-react/src/SearchNav/EmphasizedText.tsx deleted file mode 100644 index 242c1f937..000000000 --- a/easy-ui-react/src/SearchNav/EmphasizedText.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { ReactNode } from "react"; -import { Text } from "../Text"; - -export type EmphasizedTextProps = { - /** - * Emphasized text content to display. - */ - children: ReactNode; -}; - -export function EmphasizedText(props: EmphasizedTextProps) { - const { children } = props; - - return {children}; -} - -EmphasizedText.displayName = "SearchNav.EmphasizedText"; diff --git a/easy-ui-react/src/SearchNav/LogoGroup.tsx b/easy-ui-react/src/SearchNav/LogoGroup.tsx index c7199d2d2..7466772bf 100644 --- a/easy-ui-react/src/SearchNav/LogoGroup.tsx +++ b/easy-ui-react/src/SearchNav/LogoGroup.tsx @@ -12,17 +12,25 @@ export type LogoGroupProps = { children: ReactNode; }; +/** + * + * @privateRemarks + * This component doesn't directly use children and instead + * reads the nodes it renders from context. This is so we can + * efficiently share the same data across various configurations. + * + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export function LogoGroup(_props: LogoGroupProps) { - const { logo, emphasizedText, selector } = useInternalSearchNavContext(); + const { logo, title, selector } = useInternalSearchNavContext(); return (
{logo} - {emphasizedText && ( + {title && ( <> - {emphasizedText} + {title} )} {selector && ( diff --git a/easy-ui-react/src/SearchNav/SearchNav.mdx b/easy-ui-react/src/SearchNav/SearchNav.mdx index 7946f3dbb..f887cb9c2 100644 --- a/easy-ui-react/src/SearchNav/SearchNav.mdx +++ b/easy-ui-react/src/SearchNav/SearchNav.mdx @@ -27,7 +27,7 @@ less than `640px`. ## Emphasized Text - + ## Selector @@ -97,9 +97,9 @@ export type SearchNavOverlayMenuProps = { -### SearchNav.EmphasizedText +### SearchNav.Title - + ### SearchNav.Search diff --git a/easy-ui-react/src/SearchNav/SearchNav.stories.tsx b/easy-ui-react/src/SearchNav/SearchNav.stories.tsx index 38213491b..3f5dfd9c4 100644 --- a/easy-ui-react/src/SearchNav/SearchNav.stories.tsx +++ b/easy-ui-react/src/SearchNav/SearchNav.stories.tsx @@ -50,7 +50,7 @@ export const Simple: Story = { }, }; -export const EmphasizedText: Story = { +export const Title: Story = { render: Template.bind({}), args: { children: ( @@ -59,7 +59,7 @@ export const EmphasizedText: Story = { - DOCS + Docs ), @@ -75,7 +75,7 @@ export const Selector: Story = { - DOCS + Docs - DOCS + Docs - DOCS + Docs - DOCS + Docs - Search Bar + Search @@ -230,7 +230,7 @@ export const FullBar: Story = { - DOCS + Docs - Search Bar + Search diff --git a/easy-ui-react/src/SearchNav/SearchNav.test.tsx b/easy-ui-react/src/SearchNav/SearchNav.test.tsx index 683b70a6b..aff090014 100644 --- a/easy-ui-react/src/SearchNav/SearchNav.test.tsx +++ b/easy-ui-react/src/SearchNav/SearchNav.test.tsx @@ -43,9 +43,9 @@ describe("", () => { ); }); - it("should support rendering Search.EmphasizedText with appropriate styles", () => { + it("should support rendering Search.Title with appropriate styles", () => { render(getSearchNav({})); - const emphasizedText = screen.getByText("DOCS"); + const emphasizedText = screen.getByText("Docs"); expect(emphasizedText).toBeInTheDocument(); expect(emphasizedText).toHaveAttribute( "class", @@ -139,7 +139,7 @@ function getSearchNav({ selectorProps = {} }) { some logo - DOCS + Docs = Omit< MenuOverlayProps, @@ -73,7 +75,7 @@ export type SearchNavProps = { * * some logo * -* DOCS +* Docs * (props: SearchNavProps) { const { menuOverlayProps, ctaMenuSymbol, children } = props; const { onlyLogoGroup, context } = useMemo(() => { + // To support the various configurations on smaller screens, + // we extract data and nodes from the components provided + // by consumers and use context to share them efficiently. const topLevelChildren = flattenChildren(children); - const logoGroupDisplayName = getDisplayNameFromReactNode( + const { logo, title, selector, selectorChildren } = getLogoGroupChildren( topLevelChildren[0], ); + const selectorLabel = getSelectorLabel(selector); - if (logoGroupDisplayName !== "SearchNav.LogoGroup") { - throw new Error("SearchNav must contain SearchNav.LogoGroup."); - } - const logoGroupElement = topLevelChildren[0] as ReactElement; - const logoGroupChildren = flattenChildren(logoGroupElement.props.children); - const logoDisplayName = getDisplayNameFromReactNode(logoGroupChildren[0]); - if (logoDisplayName !== "SearchNav.Logo") { - throw new Error("SearchNav.LogoGroup must contain SearchNav.Logo."); - } - const logo = logoGroupChildren[0]; - - let emphasizedText; - if ( - logoGroupChildren.length > 1 && - getDisplayNameFromReactNode(logoGroupChildren[1]) === - "SearchNav.EmphasizedText" - ) { - emphasizedText = logoGroupChildren[1]; - } - - let selector; - let selectorChildren; - let selectorLabel = ""; - if ( - logoGroupChildren.length > 1 && - getDisplayNameFromReactNode( - logoGroupChildren[logoGroupChildren.length - 1], - ) === "SearchNav.Selector" - ) { - selector = logoGroupChildren[logoGroupChildren.length - 1]; - const selectorElem = selector as ReactElement; - const { "aria-label": label } = selectorElem.props; - selectorLabel = label; - selectorChildren = flattenChildren(selectorElem.props.children); - } + const search = + topLevelChildren.length > 1 + ? getSearchChildren(topLevelChildren[1]) + : undefined; - let search; - if ( - topLevelChildren.length > 1 && - getDisplayNameFromReactNode(topLevelChildren[1]) === "SearchNav.Search" - ) { - const searchChildren = flattenChildren(topLevelChildren[1]); - if (searchChildren.length === 1) { - search = searchChildren[0]; - } - } - - let secondaryCTAItems; - let primaryCTAItem; - let hasCtaGroup = false; - if ( - topLevelChildren.length > 1 && - getDisplayNameFromReactNode( - topLevelChildren[topLevelChildren.length - 1], - ) === "SearchNav.CTAGroup" - ) { - const ctaGroupElement = topLevelChildren[ - topLevelChildren.length - 1 - ] as ReactElement; - hasCtaGroup = true; - secondaryCTAItems = filterChildrenByDisplayName( - ctaGroupElement.props.children, - "SearchNav.SecondaryCTAItem", - ); - const primaryCTAItemArr = filterChildrenByDisplayName( - ctaGroupElement.props.children, - "SearchNav.PrimaryCTAItem", - ); - - if (primaryCTAItemArr.length > 1) { - throw new Error( - "SearchNav.CTAGroup can support at most one SearchNav.PrimaryCTAItem.", - ); - } - if (primaryCTAItemArr.length !== 0) { - primaryCTAItem = primaryCTAItemArr[0]; - } - } + const { secondaryCTAItems, primaryCTAItem } = getCTAGroupChildren( + topLevelChildren[topLevelChildren.length - 1], + ); - const onlyLogoGroup = !hasCtaGroup && search === undefined; + const onlyLogoGroup = + secondaryCTAItems === undefined && + primaryCTAItem === undefined && + search === undefined; return { onlyLogoGroup, context: { logo, - emphasizedText, + title, selector, selectorChildren, secondaryCTAItems, @@ -250,12 +189,12 @@ SearchNav.LogoGroup = LogoGroup; SearchNav.Logo = Logo; /** - * Represents `. + * Represents `. * * @remarks * Renders emphasized text. */ -SearchNav.EmphasizedText = EmphasizedText; +SearchNav.Title = Title; /** * Represents `. diff --git a/easy-ui-react/src/SearchNav/Title.module.scss b/easy-ui-react/src/SearchNav/Title.module.scss new file mode 100644 index 000000000..2c8c159eb --- /dev/null +++ b/easy-ui-react/src/SearchNav/Title.module.scss @@ -0,0 +1,3 @@ +.title { + text-transform: uppercase; +} diff --git a/easy-ui-react/src/SearchNav/Title.tsx b/easy-ui-react/src/SearchNav/Title.tsx new file mode 100644 index 000000000..ca097e995 --- /dev/null +++ b/easy-ui-react/src/SearchNav/Title.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode } from "react"; +import { Text } from "../Text"; +import { classNames } from "../utilities/css"; + +import styles from "./Title.module.scss"; + +export type TitleProps = { + /** + * Emphasized text content to display. + */ + children: ReactNode; +}; + +export function Title(props: TitleProps) { + const { children } = props; + + return ( + + {children} + + ); +} + +Title.displayName = "SearchNav.Title"; diff --git a/easy-ui-react/src/SearchNav/context.ts b/easy-ui-react/src/SearchNav/context.ts index 01a9d461a..d289c890f 100644 --- a/easy-ui-react/src/SearchNav/context.ts +++ b/easy-ui-react/src/SearchNav/context.ts @@ -4,7 +4,7 @@ import { IconSymbol } from "../types"; type InternalSearchNavContextType = { logo: ReactNode; - emphasizedText?: ReactNode; + title?: ReactNode; selector?: ReactNode; selectorChildren?: ReactNode[]; primaryCTAItem?: ReactNode; diff --git a/easy-ui-react/src/SearchNav/utilities.ts b/easy-ui-react/src/SearchNav/utilities.ts new file mode 100644 index 000000000..3e57ada6e --- /dev/null +++ b/easy-ui-react/src/SearchNav/utilities.ts @@ -0,0 +1,100 @@ +import { ReactElement, ReactNode } from "react"; +import { + getDisplayNameFromReactNode, + filterChildrenByDisplayName, + flattenChildren, +} from "../utilities/react"; + +export function getLogoGroupChildren(logoGroup: ReactNode) { + const logoGroupDisplayName = getDisplayNameFromReactNode(logoGroup); + + if (logoGroupDisplayName !== "SearchNav.LogoGroup") { + throw new Error("SearchNav must contain SearchNav.LogoGroup."); + } + const logoGroupElement = logoGroup as ReactElement; + const logoGroupChildren = flattenChildren(logoGroupElement.props.children); + const logoDisplayName = getDisplayNameFromReactNode(logoGroupChildren[0]); + if (logoDisplayName !== "SearchNav.Logo") { + throw new Error("SearchNav.LogoGroup must contain SearchNav.Logo."); + } + const logo = logoGroupChildren[0]; + + let title; + if ( + logoGroupChildren.length > 1 && + getDisplayNameFromReactNode(logoGroupChildren[1]) === "SearchNav.Title" + ) { + title = logoGroupChildren[1]; + } + + let selector; + let selectorChildren; + if ( + logoGroupChildren.length > 1 && + getDisplayNameFromReactNode( + logoGroupChildren[logoGroupChildren.length - 1], + ) === "SearchNav.Selector" + ) { + selector = logoGroupChildren[logoGroupChildren.length - 1]; + const selectorElem = selector as ReactElement; + selectorChildren = flattenChildren(selectorElem.props.children); + } + + return { + logo, + title, + selector, + selectorChildren, + }; +} + +export function getSearchChildren(searchParent: ReactNode) { + let search; + if (getDisplayNameFromReactNode(searchParent) === "SearchNav.Search") { + const searchChildren = flattenChildren(searchParent); + if (searchChildren.length === 1) { + search = searchChildren[0]; + } + } + return search; +} + +export function getCTAGroupChildren(ctaGroup: ReactNode) { + let secondaryCTAItems; + let primaryCTAItem; + if (getDisplayNameFromReactNode(ctaGroup) === "SearchNav.CTAGroup") { + const ctaGroupElement = ctaGroup as ReactElement; + secondaryCTAItems = filterChildrenByDisplayName( + ctaGroupElement.props.children, + "SearchNav.SecondaryCTAItem", + ); + const primaryCTAItemArr = filterChildrenByDisplayName( + ctaGroupElement.props.children, + "SearchNav.PrimaryCTAItem", + ); + + if (primaryCTAItemArr.length > 1) { + throw new Error( + "SearchNav.CTAGroup can support at most one SearchNav.PrimaryCTAItem.", + ); + } + if (primaryCTAItemArr.length !== 0) { + primaryCTAItem = primaryCTAItemArr[0]; + } + } + + return { + secondaryCTAItems, + primaryCTAItem, + }; +} + +export function getSelectorLabel(selector: ReactNode) { + let selectorLabel; + if (selector) { + const selectorElem = selector as ReactElement; + const { "aria-label": label } = selectorElem.props; + selectorLabel = label; + } + return selectorLabel; +}