Skip to content

Commit

Permalink
SearchNav feat: add new CTA and text (#742)
Browse files Browse the repository at this point in the history
## πŸ“ Changes

- adds `PrimaryCTAItem`
- original `CTAItem` renamed to `SecondaryCTAItem`
- adds `EmphasizedText` to handle rendering of text in `LogoGroup`

## βœ… Checklist

- [x] Visuals are complete and match Figma
- [x] Code is complete and in accordance with our style guide
- [x] Design and theme tokens are audited for any relevant changes
- [x] Unit tests are written and passing
- [x] TSDoc is written or updated for any component API surface area
- [x] Stories in Storybook accompany any relevant component changes
- [x] Ensure no accessibility violations are reported in Storybook
- [x] Specs and documentation are up-to-date
- [x] Cross-browser check is performed (Chrome, Safari, Firefox)
- [x] Changeset is added
  • Loading branch information
OskiTheCoder authored Oct 31, 2023
1 parent 5ecd325 commit 0cfafc2
Show file tree
Hide file tree
Showing 19 changed files with 569 additions and 181 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-cups-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@easypost/easy-ui": minor
---

feat(SearchNav): support PrimaryCTAItem and Title components
31 changes: 21 additions & 10 deletions documentation/specs/SearchNav.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ 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, 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 individual CTAs via `SearchNav.CTAItem`, which will make use of Easy UI's `UnstyledButton` component.
`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.

`SearchNav` will also need to handle a unique configuration for smaller devices. Although it won't be exposed to consumers directly,this will be accomplished via a `SearchNavMobile` component, which will be responsible for rendering a clickable hamburger and search icon. The hamburger icon will effectively be a trigger to render a menu comprised of `SearchNav.Selector` and the CTAs in `SearchNav.CTAGroup`. The clickable search icon will render the contents of `SearchNav.Search` and a right aligned close button.

Expand Down Expand Up @@ -78,22 +78,29 @@ export type SelectorProps<T> = AriaSelectProps<T> &

export type CTAGroupProps = {
/**
* The children of the <SearchNav.CTAGroup> element. Should include <SearchNav.CTAItem> elements.
* The children of the <SearchNav.CTAGroup> element. Should include <SearchNav.SecondaryCTAItem>
* elements and <SearchNav.PrimaryCTAItem>
*/
children: ReactNode;
};

export type CTAItemProps = AriaButtonProps<"button"> & {
/**
* Icon symbol SVG source from @easypost/easy-ui-icons.
*/
symbol?: IconSymbol;
/**
* Text content to display.
*/
label: string;
};

export type PrimaryCTAItemProps = CTAItemProps;

export type SecondaryCTAItemProps = CTAItemProps & {
/**
* Icon symbol SVG source from @easypost/easy-ui-icons.
*/
symbol?: IconSymbol;
/**
* Hides label on desktop.
* @default false
*/
hideLabelOnDesktop?: boolean;
/**
Expand Down Expand Up @@ -145,9 +152,13 @@ function App() {
<SearchComponent />
</SearchNav.Search>
<SearchNav.CTAGroup>
<SearchNav.CTAItem symbol={Campaign} key="Campaign" label="Optional" />
<SearchNav.CTAItem symbol={Help} key="Help" label="Optional" />
<SearchNav.CTAItem
<SearchNav.SecondaryCTAItem
symbol={Campaign}
key="Campaign"
label="Optional"
/>
<SearchNav.SecondaryCTAItem symbol={Help} key="Help" label="Optional" />
<SearchNav.SecondaryCTAItem
symbol={Brightness5}
key="Brightness"
label="Toggle theme"
Expand Down
2 changes: 1 addition & 1 deletion easy-ui-react/src/Menu/MenuSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function MenuSectionContent<T>({
return (
<>
{section.key !== state.collection.getFirstKey() &&
section.props.children && (
section.nextKey !== state.collection.getLastKey() && (
<li {...separatorProps} className={styles.separator} />
)}
<li {...itemProps}>
Expand Down
44 changes: 31 additions & 13 deletions easy-ui-react/src/SearchNav/CTAGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode, useMemo, Fragment, ReactElement } from "react";
import React, { ReactNode, Fragment, ReactElement } from "react";
import Help from "@easypost/easy-ui-icons/Help";
import { Separator } from "./Separator";
import { Menu } from "../Menu";
Expand All @@ -7,31 +7,37 @@ import { Icon } from "../Icon";
import { UnstyledButton } from "../UnstyledButton";
import { useInternalSearchNavContext } from "./context";
import { classNames } from "../utilities/css";
import { flattenChildren, getFlattenedKey } from "../utilities/react";
import { getFlattenedKey } from "../utilities/react";

import styles from "./CTAGroup.module.scss";

export type CTAGroupProps = {
/**
* The children of the <SearchNav.CTAGroup> element. Should include <SearchNav.CTAItem> elements.
* The children of the <SearchNav.CTAGroup> element. Should include <SearchNav.SecondaryCTAItem>
* elements and <SearchNav.PrimaryCTAItem>
*/
children: ReactNode;
};

export function CTAGroup(props: CTAGroupProps) {
const { children } = props;
const { menuOverlayProps, ctaMenuSymbol } = useInternalSearchNavContext();
/**
*
* @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 } =
useInternalSearchNavContext();

const items = useMemo(() => {
return flattenChildren(children);
}, [children]);

const totalItems = items.length;
const totalItems = secondaryCTAItems?.length || 0;

return (
<>
<div className={classNames(styles.ctaGroup, styles.ctaGroupExpanded)}>
{items.map((item, index) => {
{secondaryCTAItems?.map((item, index) => {
const isLastChild = index === totalItems - 1;
const itemEle = item as ReactElement;
return (
Expand All @@ -41,6 +47,12 @@ export function CTAGroup(props: CTAGroupProps) {
</Fragment>
);
})}
{primaryCTAItem && (
<>
<Separator group="cta" />
{primaryCTAItem}
</>
)}
</div>
<div className={classNames(styles.ctaGroup, styles.ctaGroupMenu)}>
<Menu>
Expand All @@ -52,7 +64,7 @@ export function CTAGroup(props: CTAGroupProps) {
</Menu.Trigger>
<Menu.Overlay placement="bottom right" {...menuOverlayProps}>
<Menu.Section aria-label="Nav actions">
{items.map((item) => {
{secondaryCTAItems?.map((item) => {
const itemEle = item as ReactElement;
return (
<Menu.Item
Expand All @@ -67,6 +79,12 @@ export function CTAGroup(props: CTAGroupProps) {
</Menu.Section>
</Menu.Overlay>
</Menu>
{primaryCTAItem && (
<>
<Separator group="cta" />
{primaryCTAItem}
</>
)}
</div>
</>
);
Expand Down
47 changes: 27 additions & 20 deletions easy-ui-react/src/SearchNav/CondensedSearchNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Close from "@easypost/easy-ui-icons/Close";
import MenuSymbol from "@easypost/easy-ui-icons/Menu";
import Search from "@easypost/easy-ui-icons/Search";
import { Menu } from "../Menu";
import { HorizontalStack } from "../HorizontalStack";
import { Text } from "../Text";
import { UnstyledButton } from "../UnstyledButton";
import { Icon } from "../Icon";
Expand All @@ -15,23 +16,24 @@ 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 `<SearchNav.Selector>` and `<SearchNav.CTAGroup>`.
* On small screens, this effectively replaces `<SearchNav>`.
*/
export function CondensedSearchNav() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);

const {
searchNode,
selectChildren,
ctaGroupChildren,
selectLabel,
search,
selectorChildren,
secondaryCTAItems,
primaryCTAItem,
selectorLabel,
menuOverlayProps,
} = useInternalSearchNavContext();

const hasMenuToShow = !!selectChildren || !!ctaGroupChildren;
const hasSearchToShow = searchNode !== null;
const hasMenuToShow = !!selectorChildren || !!secondaryCTAItems;
const hasSearchToShow = !!search;

return (
<div className={classNames(styles.condensed)}>
Expand All @@ -51,8 +53,8 @@ export function CondensedSearchNav() {
</UnstyledButton>
</Menu.Trigger>
<Menu.Overlay placement="bottom left" {...menuOverlayProps}>
<Menu.Section aria-label={selectLabel}>
{selectChildren?.map((item) => {
<Menu.Section aria-label={selectorLabel}>
{selectorChildren?.map((item) => {
const itemEle = item as ReactElement;
return (
<Menu.Item key={getFlattenedKey(itemEle.key)}>
Expand All @@ -62,7 +64,7 @@ export function CondensedSearchNav() {
})}
</Menu.Section>
<Menu.Section aria-label="Nav actions">
{ctaGroupChildren?.map((item) => {
{secondaryCTAItems?.map((item) => {
const itemEle = item as ReactElement;
return (
<Menu.Item
Expand All @@ -77,20 +79,25 @@ export function CondensedSearchNav() {
</Menu.Section>
</Menu.Overlay>
</Menu>
{searchNode && (
<UnstyledButton
className={classNames(styles.btn, styles.searchBtn)}
onPress={() => setIsSearchOpen((prev) => !prev)}
>
<Icon symbol={Search} />
<Text visuallyHidden>search</Text>
</UnstyledButton>
{(search || primaryCTAItem) && (
<HorizontalStack gap="2">
{search && (
<UnstyledButton
className={classNames(styles.btn, styles.searchBtn)}
onPress={() => setIsSearchOpen((prev) => !prev)}
>
<Icon symbol={Search} />
<Text visuallyHidden>search</Text>
</UnstyledButton>
)}
{primaryCTAItem}
</HorizontalStack>
)}
</>
) : (
hasSearchToShow && (
<div className={classNames(styles.condensedSearch)}>
{searchNode}
{search}
<UnstyledButton
className={classNames(styles.btn)}
onPress={() => setIsSearchOpen((prev) => !prev)}
Expand Down
35 changes: 21 additions & 14 deletions easy-ui-react/src/SearchNav/LogoGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { ReactNode, useMemo } from "react";
import React, { ReactNode } from "react";
import { Separator } from "./Separator";
import { flattenChildren } from "../utilities/react";
import { classNames } from "../utilities/css";

import styles from "./LogoGroup.module.scss";
import { useInternalSearchNavContext } from "./context";

export type LogoGroupProps = {
/**
Expand All @@ -13,23 +12,31 @@ export type LogoGroupProps = {
children: ReactNode;
};

export function LogoGroup(props: LogoGroupProps) {
const { children } = props;

const items = useMemo(() => {
return flattenChildren(children);
}, [children]);

const logo = items[0];
const select = items.length === 2 ? items[1] : null;
/**
*
* @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, title, selector } = useInternalSearchNavContext();

return (
<div className={classNames(styles.logoGroup)}>
{logo}
{select && (
{title && (
<>
<Separator group="logo" />
{title}
</>
)}
{selector && (
<>
<Separator group="logo" />
{select}
{selector}
</>
)}
</div>
Expand Down
24 changes: 24 additions & 0 deletions easy-ui-react/src/SearchNav/PrimaryCTAItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { AriaButtonProps } from "react-aria";
import { Button } from "../Button";

export type CTAItemProps = AriaButtonProps<"button"> & {
/**
* Text content to display.
*/
label: string;
};

export type PrimaryCTAItemProps = CTAItemProps;

export function PrimaryCTAItem(props: PrimaryCTAItemProps) {
const { label, ...restProps } = props;

return (
<Button {...restProps} variant="outlined" size="sm" color="support">
{label}
</Button>
);
}

PrimaryCTAItem.displayName = "SearchNav.PrimaryCTAItem";
Loading

0 comments on commit 0cfafc2

Please sign in to comment.