,
+) {
+ return (
+
+ )
+}
+const IconButton = forwardRef(IconButtonEl)
+
+export function BasicNavMenuPreview() {
+ return (
+
+ Features
+
+ Something
+
+
+ )
+}
+
+export function CustomNavMenuPreview() {
+ return (
+
+
+ Features
+
+
+ Something
+
+
+ )
+}
+
+export function PositionNavMenuPreview() {
+ return (
+
+ Features
+
+ RightPosition
+
+
+ )
+}
diff --git a/docs/app/react/nav-menu/doc.mdx b/docs/app/react/nav-menu/doc.mdx
new file mode 100644
index 00000000..98ab864d
--- /dev/null
+++ b/docs/app/react/nav-menu/doc.mdx
@@ -0,0 +1,173 @@
+---
+npm: '@cerberus-design/react'
+source: 'context/navMenu.tsx'
+recipe: ''
+---
+
+import {
+ NoteAdmonition,
+ WhenToUseAdmonition,
+ WhenNotToUseAdmonition,
+} from '@/app/components/Admonition'
+import CodePreview from '@/app/components/CodePreview'
+import { BasicNavMenuPreview, CustomNavMenuPreview, PositionNavMenuPreview } from '@/app/react/nav-menu/components/nav-menu-preview'
+
+# Nav Menu
+
+A menu that uses a [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) to show and hide navigation links.
+
+```ts
+import {
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ NavMenuLink
+} from '@cerberus-design/react'
+```
+
+
+
+## Usage
+
+
}>
+```tsx title="nav.tsx"
+import {
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ NavMenuLink
+} from '@cerberus-design/react'
+
+function BasicNavMenuPreview() {
+ return (
+
+ Features
+
+ Something
+
+
+ )
+}
+```
+
+
+## NextJS Usage
+
+To use the `NavMenuLink` component with NextJS, you should use the `Link` component from `next/link` for the `as` prop.
+
+
}>
+```tsx title="nav.tsx" {8,15}
+import {
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ NavMenuLink
+} from '@cerberus-design/react'
+import MoreOptionsButton from './MoreOptionsButton'
+import Link from '@next/link'
+
+function BasicNavMenuPreview() {
+ return (
+
+ Features
+
+ Something
+
+
+ )
+}
+```
+
+
+## Positions
+
+The `NavMenuList` component accepts a `position` prop to determine where the menu will be positioned relative to the trigger. [See available positions](#navmenulist).
+
+
}>
+```tsx title="nav.tsx" {12}
+import {
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ NavMenuLink
+} from '@cerberus-design/react'
+
+function PositionNavMenuPreview() {
+ return (
+
+ Features
+
+ RightPosition
+
+
+ )
+}
+```
+
+
+## API
+
+### NavMenuTrigger
+
+```ts showLineNumbers=false
+export interface NavMenuTriggerProps
+ extends ButtonHTMLAttributes
,
+ ButtonProps,
+ NavTriggerAriaValues {
+ as?: ElementType
+}
+
+define function NavMenuTrigger(props: NavMenuTriggerProps): ReactNode | null
+```
+
+#### Props
+
+The `NavMenuTrigger` component accepts the following props in addition to the props of the [Button](./button) component:
+
+| Name | Default | Description |
+| -------- | ------- | ------------------------------------------------------------- |
+| as | null | A custom component to render as the Trigger. |
+| controls | null | REQUIRED. The `id` of the menu the trigger will control. |
+| expanded | false | A custom component to render as the Trigger. |
+
+
+
+### NavMenuList
+
+```ts showLineNumbers=false
+export interface NavMenuListProps extends HTMLAttributes {
+ id: string
+}
+
+define function NavMenuList(props: NavMenuListProps): ReactNode | null
+```
+
+#### Props
+
+The `NavMenuList` component accepts the following props:
+
+| Name | Default | Description |
+| -------- | ------- | ------------------------------------------------------------- |
+| id | null | REQUIRED. A custom `id` to associate with the trigger. |
+| position | `left` | The position of the menu relative to the trigger. Can be `left`, `right`, `top`, or `bottom`. |
+
+### NavMenuLink
+
+```ts showLineNumbers=false
+export interface NavMenuLinkProps
+ extends AnchorHTMLAttributes {
+ as?: ElementType
+}
+
+define function NavMenuLink(props: NavMenuLinkProps): ReactNode | null
+```
+
+#### Props
+
+The `NavMenuLink` component accepts the following props:
+
+| Name | Default | Description |
+| -------- | ------- | ------------------------------------------------------------- |
+| as | null | A custom component to render as the Trigger. |
+
+
\ No newline at end of file
diff --git a/docs/app/react/nav-menu/page.tsx b/docs/app/react/nav-menu/page.tsx
new file mode 100644
index 00000000..bdca81fd
--- /dev/null
+++ b/docs/app/react/nav-menu/page.tsx
@@ -0,0 +1,21 @@
+import ApiLinks from '@/app/components/ApiLinks'
+import OnThisPage from '../../components/OnThisPage'
+import { PageMainContent, PageSections } from '../../components/PageLayout'
+import Doc, { frontmatter } from './doc.mdx'
+
+export default function NavMenuPage() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/docs/app/react/show/doc.mdx b/docs/app/react/show/doc.mdx
index 1d802185..f6d909c1 100644
--- a/docs/app/react/show/doc.mdx
+++ b/docs/app/react/show/doc.mdx
@@ -73,7 +73,7 @@ define function Show(props: ShowProps): ReactNode | null
### Props
-The `Show` component the following props:
+The `Show` component accepts the following props:
| Name | Default | Description |
| -------- | ------- | ------------------------------------------------------------- |
diff --git a/docs/app/react/side-nav.json b/docs/app/react/side-nav.json
index 2a06b13e..d4430c64 100644
--- a/docs/app/react/side-nav.json
+++ b/docs/app/react/side-nav.json
@@ -25,6 +25,13 @@
},
{
"id": "2:b",
+ "label": "Nav Menu",
+ "route": "/react/nav-menu",
+ "tag": "",
+ "type": "route"
+ },
+ {
+ "id": "2:c",
"label": "Show",
"route": "/react/show",
"tag": "",
diff --git a/packages/react/src/aria-helpers/nav-menu.aria.ts b/packages/react/src/aria-helpers/nav-menu.aria.ts
new file mode 100644
index 00000000..dc1ea32f
--- /dev/null
+++ b/packages/react/src/aria-helpers/nav-menu.aria.ts
@@ -0,0 +1,11 @@
+export interface NavTriggerAriaValues {
+ controls: string
+ expanded?: boolean
+}
+
+export function createNavTriggerProps(values: NavTriggerAriaValues) {
+ return {
+ ['aria-controls']: values.controls,
+ ['aria-expanded']: values.expanded ?? false,
+ }
+}
diff --git a/packages/react/src/components/Button.tsx b/packages/react/src/components/Button.tsx
index df17ce6c..3ef226ca 100644
--- a/packages/react/src/components/Button.tsx
+++ b/packages/react/src/components/Button.tsx
@@ -8,6 +8,10 @@ export interface ButtonProps extends ButtonHTMLAttributes {
shape?: 'sharp' | 'rounded'
}
+/**
+ * A component that allows the user to perform actions
+ * @description https://github.com/omnifed/cerberus/blob/main/packages/react/src/components/Button.tsx
+ */
export function Button(props: ButtonProps) {
const { palette, usage, shape, ...nativeProps } = props
return (
diff --git a/packages/react/src/components/NavMenuLink.tsx b/packages/react/src/components/NavMenuLink.tsx
new file mode 100644
index 00000000..f5b10bc1
--- /dev/null
+++ b/packages/react/src/components/NavMenuLink.tsx
@@ -0,0 +1,21 @@
+import type { AnchorHTMLAttributes, ElementType } from 'react'
+import { Show } from './Show'
+
+export interface NavMenuLinkProps
+ extends AnchorHTMLAttributes {
+ as?: ElementType
+}
+
+export function NavMenuLink(props: NavMenuLinkProps) {
+ const { as, ...nativeProps } = props
+ const hasAs = Boolean(as)
+ const AsSub: ElementType = as!
+
+ return (
+
+ }>
+ {hasAs && }
+
+
+ )
+}
diff --git a/packages/react/src/components/NavMenuList.tsx b/packages/react/src/components/NavMenuList.tsx
new file mode 100644
index 00000000..d2fde427
--- /dev/null
+++ b/packages/react/src/components/NavMenuList.tsx
@@ -0,0 +1,61 @@
+'use client'
+
+import { useMemo, type HTMLAttributes } from 'react'
+import { useNavMenuContext } from '../context/navMenu'
+import { Show } from './Show'
+import type { Positions } from '../types'
+import { css, cx } from '@cerberus-design/styled-system/css'
+
+export function getPosition(position: Positions) {
+ const defaultPositions = {
+ left: 'auto',
+ right: 'auto',
+ top: 'auto',
+ bottom: 'auto',
+ }
+ switch (position) {
+ case 'right':
+ return { ...defaultPositions, bottom: '50%', left: '110%' }
+ case 'left':
+ return { ...defaultPositions, bottom: '50%', right: '110%' }
+ case 'bottom':
+ return { ...defaultPositions, top: '110%' }
+ case 'top':
+ return { ...defaultPositions, bottom: '110%' }
+ default:
+ return defaultPositions
+ }
+}
+
+//
+
+export interface NavMenuListProps extends HTMLAttributes {
+ id: string
+ position?: Positions
+}
+
+export function NavMenuList(props: NavMenuListProps) {
+ const { position, ...nativeProps } = props
+ const { menuRef, expanded } = useNavMenuContext()
+ const locationStyles = useMemo(
+ () => getPosition(position ?? 'bottom'),
+ [position],
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/packages/react/src/components/NavMenuTrigger.tsx b/packages/react/src/components/NavMenuTrigger.tsx
new file mode 100644
index 00000000..639f841f
--- /dev/null
+++ b/packages/react/src/components/NavMenuTrigger.tsx
@@ -0,0 +1,89 @@
+'use client'
+
+import {
+ useCallback,
+ type ButtonHTMLAttributes,
+ type ElementType,
+ type MouseEvent,
+} from 'react'
+import { cx } from '@cerberus-design/styled-system/css'
+import { button } from '@cerberus-design/styled-system/recipes'
+import {
+ createNavTriggerProps,
+ type NavTriggerAriaValues,
+} from '../aria-helpers/nav-menu.aria'
+import { Show } from './Show'
+import type { ButtonProps } from './Button'
+import { useNavMenuContext } from '../context/navMenu'
+
+export interface NavMenuTriggerProps
+ extends ButtonHTMLAttributes,
+ ButtonProps,
+ NavTriggerAriaValues {
+ as?: ElementType
+}
+
+/**
+ * A component that allows the user to trigger a navigation menu.
+ * @description https://github.com/omnifed/cerberus/blob/main/packages/react/src/components/NavMenuTrigger.tsx
+ */
+export function NavMenuTrigger(props: NavMenuTriggerProps) {
+ const {
+ as,
+ palette,
+ usage,
+ shape,
+ controls,
+ expanded: propsExpanded,
+ onClick,
+ ...nativeProps
+ } = props
+ const { triggerRef, onToggle, expanded } = useNavMenuContext()
+ const ariaProps = createNavTriggerProps({
+ controls,
+ expanded: propsExpanded ?? expanded,
+ })
+ const hasAs = Boolean(as)
+ const AsSub: ElementType = as!
+
+ const handleClick = useCallback(
+ (e: MouseEvent) => {
+ if (onClick) return onClick(e)
+ onToggle()
+ },
+ [onClick, onToggle],
+ )
+
+ return (
+
+ {props.children}
+
+ }
+ >
+ {hasAs && (
+
+ )}
+
+ )
+}
diff --git a/packages/react/src/context/navMenu.tsx b/packages/react/src/context/navMenu.tsx
new file mode 100644
index 00000000..2758c014
--- /dev/null
+++ b/packages/react/src/context/navMenu.tsx
@@ -0,0 +1,65 @@
+'use client'
+
+import { css } from '@cerberus-design/styled-system/css'
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useMemo,
+ useRef,
+ useState,
+ type PropsWithChildren,
+ type RefObject,
+} from 'react'
+
+export type NavTriggerRef = RefObject
+export type NavMenuRef = RefObject
+
+export interface NavMenuContextValue {
+ triggerRef: NavTriggerRef | null
+ menuRef: NavMenuRef | null
+ expanded: boolean
+ onToggle: () => void
+}
+
+const NavMenuContext = createContext(null)
+
+export function NavMenu(props: PropsWithChildren) {
+ const triggerRef = useRef(null)
+ const menuRef = useRef(null)
+ const [expanded, setExpanded] = useState(false)
+
+ const handleToggle = useCallback(() => {
+ setExpanded((prev) => !prev)
+ }, [])
+
+ const value = useMemo(
+ () => ({
+ triggerRef,
+ menuRef,
+ expanded,
+ onToggle: handleToggle,
+ }),
+ [expanded, handleToggle],
+ )
+
+ return (
+
+
+
+ )
+}
+
+export function useNavMenuContext() {
+ const context = useContext(NavMenuContext)
+ if (!context) {
+ throw new Error('useNavMenuContext must be used within a NavMenu.')
+ }
+ return context
+}
diff --git a/packages/react/src/context/theme.tsx b/packages/react/src/context/theme.tsx
index 6b10c5c5..f4d35802 100644
--- a/packages/react/src/context/theme.tsx
+++ b/packages/react/src/context/theme.tsx
@@ -17,15 +17,9 @@ export interface ThemeContextValue {
export const THEME_KEY = 'cerberus-theme'
export const MODE_KEY = 'cerberus-mode'
-const initialThemeState = {
- theme: 'cerberus' as const,
- mode: 'light' as const,
- updateTheme: () => {},
- updateMode: () => {},
-}
-
-const ThemeContext =
- createContext>(initialThemeState)
+const ThemeContext = createContext | null>(
+ null,
+)
export function ThemeProvider(props: PropsWithChildren) {
const state = useTheme()
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 98801ccd..12e4a0f5 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,12 +1,24 @@
// components
export * from './components/Button'
+export * from './components/NavMenuTrigger'
+export * from './components/NavMenuList'
+export * from './components/NavMenuLink'
export * from './components/Show'
// context
+export * from './context/navMenu'
export * from './context/theme'
// hooks
export * from './hooks/useTheme'
+
+// aria-helpers
+
+export * from './aria-helpers/nav-menu.aria'
+
+// shared types
+
+export * from './types'
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
new file mode 100644
index 00000000..9d27b1f9
--- /dev/null
+++ b/packages/react/src/types.ts
@@ -0,0 +1 @@
+export type Positions = 'top' | 'right' | 'bottom' | 'left'
diff --git a/tests/react/aria-helpers/navMenuAria.test.ts b/tests/react/aria-helpers/navMenuAria.test.ts
new file mode 100644
index 00000000..83ccdffd
--- /dev/null
+++ b/tests/react/aria-helpers/navMenuAria.test.ts
@@ -0,0 +1,20 @@
+import { describe, test, expect } from 'bun:test'
+import { createNavTriggerProps } from '@cerberus-design/react'
+
+describe('navMenu aria helpers', () => {
+ test('should export trigger helpers', () => {
+ expect(createNavTriggerProps).toBeFunction()
+ })
+
+ test('should return an object with aria-controls and aria-expanded', () => {
+ const values = {
+ controls: 'menu',
+ expanded: false,
+ }
+
+ expect(createNavTriggerProps(values)).toEqual({
+ 'aria-controls': 'menu',
+ 'aria-expanded': false,
+ })
+ })
+})
diff --git a/tests/react/components/NavMenuTrigger.test.tsx b/tests/react/components/NavMenuTrigger.test.tsx
new file mode 100644
index 00000000..22d7c0e8
--- /dev/null
+++ b/tests/react/components/NavMenuTrigger.test.tsx
@@ -0,0 +1,208 @@
+import { describe, test, expect, afterEach, jest } from 'bun:test'
+import { cleanup, render, screen } from '@testing-library/react'
+import { NavMenu, NavMenuTrigger } from '@cerberus-design/react'
+import { setupStrictMode, user } from '@/utils'
+import { forwardRef, type ForwardedRef, type PropsWithChildren } from 'react'
+
+describe('NavMenuTrigger', () => {
+ setupStrictMode()
+ afterEach(cleanup)
+
+ test('should render a default button element', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ render(it works, {
+ wrapper: NavMenu,
+ })
+ expect(screen.getByText(/it works/i)).toBeTruthy()
+ expect(screen.getByText(/it works/i).getAttribute('aria-controls')).toBe(
+ 'menu',
+ )
+ expect(screen.getByText(/it works/i).getAttribute('aria-expanded')).toBe(
+ 'false',
+ )
+ })
+
+ test('should render a action button', () => {
+ const ariaProps = {
+ controls: 'menu-1',
+ expanded: true,
+ }
+ render(it works, {
+ wrapper: NavMenu,
+ })
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--palette_action'),
+ ).toBeTrue()
+ })
+
+ test('should render a danger button', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--palette_danger'),
+ ).toBeTrue()
+ })
+
+ test('should render a text button', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--usage_text'),
+ ).toBeTrue()
+ })
+
+ test('should render an outline button', () => {
+ const ariaProps = {
+ controls: 'menu-3',
+ expanded: true,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--usage_outline'),
+ ).toBeTrue()
+ })
+
+ test('should render a filled button', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--usage_filled'),
+ ).toBeTrue()
+ })
+
+ test('should render a sharp button', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--shape_sharp'),
+ ).toBeTrue()
+ })
+
+ test('should render a rounded button', () => {
+ const ariaProps = {
+ controls: 'menu-4',
+ expanded: true,
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(
+ screen
+ .getByText(/it works/i)
+ .classList.contains('cerberus-button--shape_rounded'),
+ ).toBeTrue()
+ })
+
+ test('should render an alternate button', () => {
+ const ariaProps = {
+ controls: 'menu',
+ expanded: false,
+ }
+ function LinkEL(
+ props: PropsWithChildren,
+ ref: ForwardedRef,
+ ) {
+ return
+ }
+ const Link = forwardRef(LinkEL)
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ expect(screen.getByText(/it works/i).tagName).toBe('A')
+ expect(screen.getByText(/it works/i).getAttribute('aria-controls')).toBe(
+ 'menu',
+ )
+ expect(screen.getByText(/it works/i).getAttribute('aria-expanded')).toBe(
+ 'false',
+ )
+ })
+
+ test('should accept a custom onClick event', async () => {
+ const handleClick = jest.fn()
+ const ariaProps = {
+ controls: 'menu',
+ }
+ render(
+
+ it works
+ ,
+ {
+ wrapper: NavMenu,
+ },
+ )
+ await user.click(screen.getByText(/it works/i))
+ expect(handleClick).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/tests/react/components/navMenuList.test.tsx b/tests/react/components/navMenuList.test.tsx
new file mode 100644
index 00000000..b0d4a49b
--- /dev/null
+++ b/tests/react/components/navMenuList.test.tsx
@@ -0,0 +1,95 @@
+import { describe, test, expect, afterEach } from 'bun:test'
+import { render, screen, cleanup, waitFor } from '@testing-library/react'
+import {
+ getPosition,
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ NavMenuLink,
+ type NavMenuListProps,
+} from '@cerberus-design/react'
+import { setupStrictMode, user } from '@/utils'
+import type { PropsWithChildren } from 'react'
+
+type Attributes = {
+ style: {
+ value: string
+ }
+}
+
+describe('NavMenuList & NavMenuLink', () => {
+ setupStrictMode()
+ afterEach(cleanup)
+
+ function Link(props: PropsWithChildren) {
+ return
+ }
+
+ function NavMenuTest(props: NavMenuListProps) {
+ return (
+
+ Trigger
+
+
+ custom link
+
+
+
+ )
+ }
+
+ // navMenu.test.tsx covers majority of use cases. This only tests fine tuning
+
+ test('should allow a custom Link component', async () => {
+ render()
+ await user.click(screen.getByText(/trigger/i))
+ await waitFor(() => expect(screen.getByText(/custom link/i)).toBeTruthy())
+ })
+
+ test('should allow a custom position', async () => {
+ render()
+ await user.click(screen.getByText(/trigger/i))
+ await waitFor(() => expect(screen.getByText(/custom link/i)).toBeTruthy())
+ screen.debug()
+ const list = screen.getByRole('list').attributes as unknown as Attributes
+ expect(list.style.value).toBe(
+ 'left: auto; right: auto; top: auto; bottom: 110%;',
+ )
+ })
+
+ test('should have a way to get the right position value', () => {
+ expect(getPosition('right')).toEqual({
+ left: '110%',
+ right: 'auto',
+ top: 'auto',
+ bottom: '50%',
+ })
+ })
+
+ test('should have a way to get the left position value', () => {
+ expect(getPosition('left')).toEqual({
+ left: 'auto',
+ right: '110%',
+ top: 'auto',
+ bottom: '50%',
+ })
+ })
+
+ test('should have a way to get the bottom position value', () => {
+ expect(getPosition('bottom')).toEqual({
+ left: 'auto',
+ right: 'auto',
+ top: '110%',
+ bottom: 'auto',
+ })
+ })
+
+ test('should have a way to get the top position value', () => {
+ expect(getPosition('top')).toEqual({
+ left: 'auto',
+ right: 'auto',
+ top: 'auto',
+ bottom: '110%',
+ })
+ })
+})
diff --git a/tests/react/context/navMenu.test.tsx b/tests/react/context/navMenu.test.tsx
new file mode 100644
index 00000000..574df01f
--- /dev/null
+++ b/tests/react/context/navMenu.test.tsx
@@ -0,0 +1,56 @@
+import { describe, test, expect, afterEach, spyOn } from 'bun:test'
+import {
+ render,
+ screen,
+ cleanup,
+ renderHook,
+ waitFor,
+} from '@testing-library/react'
+import {
+ NavMenu,
+ NavMenuTrigger,
+ NavMenuList,
+ useNavMenuContext,
+ NavMenuLink,
+} from '@cerberus-design/react'
+import { setupStrictMode, user } from '@/utils'
+
+describe('navMenu & useNavMenuContext', () => {
+ setupStrictMode()
+ afterEach(cleanup)
+
+ test('should export a NavMenu', () => {
+ render(
+
+ Open
+
+ first link
+
+ ,
+ )
+ expect(screen.getByText(/open/i)).toBeTruthy()
+ expect(screen.queryByText(/first link/i)).toBeNull()
+ })
+
+ test('should render a NavMenuList if expanded', async () => {
+ render(
+
+ View
+
+ first link
+
+ ,
+ )
+ expect(screen.queryByText(/first link/i)).toBeNull()
+ await user.click(screen.getByText(/view/i))
+ await waitFor(() => expect(screen.getByText(/first link/i)).toBeTruthy())
+ })
+
+ test('should throw an error if used outside of NavMenu', () => {
+ // don't clog up the console with errors
+ spyOn(console, 'error').mockImplementation(() => null)
+ expect(() => renderHook(() => useNavMenuContext())).toThrow(
+ 'useNavMenuContext must be used within a NavMenu',
+ )
+ })
+})
diff --git a/tests/react/context/theme.test.tsx b/tests/react/context/theme.test.tsx
index de5aa1f3..fcf93d1d 100644
--- a/tests/react/context/theme.test.tsx
+++ b/tests/react/context/theme.test.tsx
@@ -1,12 +1,4 @@
-import {
- describe,
- test,
- expect,
- afterEach,
- beforeEach,
- mock,
- spyOn,
-} from 'bun:test'
+import { describe, test, expect, afterEach, beforeEach, spyOn } from 'bun:test'
import { render, screen, cleanup, renderHook } from '@testing-library/react'
import {
useThemeContext,
@@ -93,13 +85,8 @@ describe('useThemeContext', () => {
test('should throw an error if used outside of ThemeProvider', () => {
// don't clog up the console with errors
spyOn(console, 'error').mockImplementation(() => null)
- mock.module('react', () => {
- return { useContext: () => null }
- })
-
expect(() => renderHook(() => useThemeContext())).toThrow(
'useThemeContext must be used within a ThemeProvider',
)
- mock.restore()
})
})