Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ yarn generate:tokens # Generate CSS variables
- Use design tokens for consistent styling
- Ensure accessibility compliance
- **All components MUST follow the namespace interface pattern (see guidelines/interface-pattern.md)**
- **React contexts MUST follow the context pattern (see guidelines/context-pattern.md)**
237 changes: 237 additions & 0 deletions guidelines/context-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# React Context Pattern Guidelines

This document outlines the standard pattern for implementing React contexts in Reapit Elements components.

## Overview

React contexts in this codebase follow a consistent namespace pattern that provides type safety, clear documentation, and proper error handling. All contexts should follow this established pattern for consistency and maintainability.

## ✅ Required Pattern

### Basic Context Structure

```typescript
import { createContext, useContext } from 'react'

export namespace ComponentNameContext {
export interface Value {
/** JSDoc documentation for each property */
property: string
/** Optional properties should be marked as such */
optionalProperty?: boolean
}
}

/**
* Brief description of what this context provides.
* Include usage examples if helpful.
*/
export const ComponentNameContext = createContext<ComponentNameContext.Value | null>(null)

/**
* Returns the current ComponentNameContext value.
* @throws an error if the context is not defined.
*/
export function useComponentNameContext(): ComponentNameContext.Value {
const context = useContext(ComponentNameContext)
if (!context) {
throw new Error('useComponentNameContext must be used within a ComponentName')
}
return context
}
```

## 🔍 Pattern Elements

### 1. Namespace Declaration

- **MUST** use the component name followed by `Context`
- **MUST** contain a `Value` interface that defines the context shape
- **MUST** document all properties with JSDoc comments

```typescript
export namespace DialogContext {
export interface Value {
/** The ID used for accessibility labeling */
titleId: string
}
}
```

### 2. Context Creation

- **MUST** use `createContext` with the namespace type union: `ComponentNameContext.Value | null`
- **MUST** initialize with `null` to enforce proper usage checking
- **SHOULD** include JSDoc documentation explaining the context's purpose

```typescript
/**
* The context available to a Dialog's descendants. Provides access to titleId
* for proper accessibility labeling.
*/
export const DialogContext = createContext<DialogContext.Value | null>(null)
```

### 3. Custom Hook

- **MUST** provide a custom hook named `useComponentNameContext`
- **MUST** throw a descriptive error if context is not available
- **MUST** return the non-null context value
- **SHOULD** include JSDoc documentation

```typescript
/**
* Returns the current DialogContext value.
* @throws an error if the context is not defined.
*/
export function useDialogContext(): DialogContext.Value {
const context = useContext(DialogContext)
if (!context) {
throw new Error('DialogContext not defined: useDialogContext can only be used in a child of DialogContext')
}
return context
}
```

## 📋 Context Value Examples

### Simple State Context

Check notice on line 98 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L98

Expected: 1; Actual: 0; Below
```typescript
export namespace BottomBarContext {
export interface Value {
/** Whether the bottom bar is currently open */
isOpen: boolean
}
}
```

### Configuration Context

Check notice on line 108 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L108

Expected: 1; Actual: 0; Below
```typescript
export namespace ChipSelectContext {
export interface Value {
/** The ID of the form to associate chip select options with */
form?: string
/** Whether the chip select allows multiple selections */
multiple: boolean
/** The name each chip select option should have */
name?: string
/** The size of options in the chip select */
size: ComponentProps<typeof ChipSelectChip>['size']
}
}
```

### Complex State Context

Check notice on line 124 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L124

Expected: 1; Actual: 0; Below
```typescript
export namespace SplitButtonContext {
export interface Value {
/** Whether the main action button, menu button, or neither, is busy */
busy: 'action' | 'menu-item' | undefined
/** The size of the main action and menu buttons */
size: ComponentProps<typeof SplitButton>['size']
/** The variant used by the main action and menu buttons */
variant: ComponentProps<typeof SplitButton>['variant']
}
}
```

## 🚫 Common Mistakes to Avoid

### ❌ Wrong Naming

Check notice on line 140 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L140

Expected: 1; Actual: 0; Below
```typescript
// Don't use inconsistent naming
export namespace ButtonCtx { } // Wrong: use full "Context"
export namespace ButtonContextState { } // Wrong: don't add suffixes
```

### ❌ Missing Error Handling

Check notice on line 147 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L147

Expected: 1; Actual: 0; Below
```typescript
// Don't return nullable values from hook
export function useDialogContext(): DialogContext.Value | null {
return useContext(DialogContext) // Wrong: should throw on null
}
```

### ❌ Inconsistent Error Messages

Check notice on line 155 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L155

Expected: 1; Actual: 0; Below
```typescript
// Don't use generic error messages
throw new Error('Context not found') // Wrong: be specific

// Use descriptive, component-specific messages
throw new Error('useDialogContext can only be used in a child of DialogContext')
```

### ❌ Missing Documentation

Check notice on line 164 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L164

Expected: 1; Actual: 0; Below
```typescript
export namespace DialogContext {
export interface Value {
titleId: string // Wrong: missing JSDoc
}
}
```

## 🎯 File Structure

Each context should be in its own file within the component directory:

```
src/core/component-name/
├── index.ts
├── component-name.tsx
├── context.tsx # Context implementation here
└── __tests__/
```

## 📝 Integration with Components

### Provider Usage

Check notice on line 187 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L187

Expected: 1; Actual: 0; Below
```typescript
// In the main component file
import { ComponentNameContext } from './context'

export function ComponentName({ children, ...props }: ComponentName.Props) {
const contextValue: ComponentNameContext.Value = {
// Initialize context values based on props/state
}

return (
<ComponentNameContext.Provider value={contextValue}>
{children}
</ComponentNameContext.Provider>
)
}
```

### Consumer Usage

Check notice on line 205 in guidelines/context-pattern.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

guidelines/context-pattern.md#L205

Expected: 1; Actual: 0; Below
```typescript
// In child components
import { useComponentNameContext } from '../context'

export function ChildComponent() {
const { property } = useComponentNameContext()
// Use context values
}
```

## 🔍 Code Review Checklist

When reviewing context implementations:

- [ ] Namespace follows `ComponentNameContext` pattern
- [ ] Interface is named `Value` and exported from namespace
- [ ] All properties have JSDoc documentation
- [ ] Context created with `| null` union type
- [ ] Custom hook throws descriptive error on null context
- [ ] Error message mentions both hook name and required parent component
- [ ] Hook returns non-nullable context value
- [ ] File follows established directory structure

## 📚 Related Patterns

This context pattern works in conjunction with:

- [Interface Pattern](./interface-pattern.md) - For component props
- Component composition patterns
- Accessibility patterns (e.g., `titleId` for ARIA labeling)

Following this pattern ensures consistency across all React contexts in the Reapit Elements library.
7 changes: 5 additions & 2 deletions src/core/bottom-bar/bottom-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BottomBarContextProvider } from './context'
import { BottomBarContext, useBottomBarContext } from './context'
import { BottomBarItemButton } from './item'
import { BottomBarMenuList } from './menu-list'
import { ElBottomBarContainer, ElBottomBarNav } from './styles'
Expand Down Expand Up @@ -37,7 +37,7 @@ export function BottomBar({
return (
<ElBottomBarContainer>
<ElBottomBarNav {...rest} aria-label={ariaLabel} data-is-open={isOpen}>
<BottomBarContextProvider isOpen={isOpen}>{children}</BottomBarContextProvider>
<BottomBarContext.Provider value={{ isOpen }}>{children}</BottomBarContext.Provider>
</ElBottomBarNav>
</ElBottomBarContainer>
)
Expand All @@ -48,5 +48,8 @@ BottomBar.ItemButton = BottomBarItemButton
BottomBar.MenuItem = BottomBarMenuList.MenuItem
BottomBar.MenuList = BottomBarMenuList

BottomBar.Context = BottomBarContext
BottomBar.useContext = useBottomBarContext

/** @deprecated Use BottomBar.Props instead */
export type BottomBarProps = BottomBar.Props
27 changes: 8 additions & 19 deletions src/core/bottom-bar/context.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,23 @@
import { createContext, useContext, useMemo } from 'react'
import { createContext, useContext } from 'react'

import type { ReactNode } from 'react'

interface BottomBarContextValue {
isOpen: boolean
}

interface BottomBarContextProviderProps extends BottomBarContextValue {
children: ReactNode
export namespace BottomBarContext {
export interface Value {
/** Whether the bottom bar is currently open */
isOpen: boolean
}
}

/**
* The context available to a BottomBar's descendants. Provides access to a single `isOpen` boolean
* that allows them to be aware of the BottomBar's open state.
*/
export const BottomBarContext = createContext<BottomBarContextValue | null>(null)

/**
* Provides the given values over the `BottomBarContext`. For internal BottomBar use only.
*/
export function BottomBarContextProvider({ children, isOpen }: BottomBarContextProviderProps) {
const value = useMemo<BottomBarContextValue>(() => ({ isOpen }), [isOpen])
return <BottomBarContext.Provider value={value}>{children}</BottomBarContext.Provider>
}
export const BottomBarContext = createContext<BottomBarContext.Value | null>(null)

/**
* Returns the current `BottomBarContext` value.
* @throws an error if the context is not defined.
*/
export function useBottomBarContext(): BottomBarContextValue {
export function useBottomBarContext(): BottomBarContext.Value {
const context = useContext(BottomBarContext)
if (!context) {
throw new Error('BottomBarContext not defined: useBottomBarContext can only be used in a child of BottomBarContext')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BottomBarContextProvider } from '../../context'
import { BottomBarContext } from '../../context'
import { BottomBarMenuListItem } from '../menu-list-menu-item'
import { Menu } from '#src/core/menu'
import { StarIcon } from '#src/icons/star'
Expand Down Expand Up @@ -57,5 +57,5 @@ interface WrapperProps {
}

function wrapper({ children }: WrapperProps) {
return <BottomBarContextProvider isOpen={true}>{children}</BottomBarContextProvider>
return <BottomBarContext.Provider value={{ isOpen: true }}>{children}</BottomBarContext.Provider>
}
10 changes: 4 additions & 6 deletions src/core/chip-select/__tests__/chip-select-option.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChipSelectOption } from '../chip-select-option'
import { ChipSelectContextProvider } from '../context'
import { ChipSelectContext } from '../context'
import { render, screen } from '@testing-library/react'

import type { ComponentProps, ReactNode } from 'react'
import type { ReactNode } from 'react'

test('renders as checkbox element', () => {
render(<ChipSelectOption value="test-value">Test Option</ChipSelectOption>, {
Expand Down Expand Up @@ -56,10 +56,8 @@ test('forwards additional props to ChipSelectChip', () => {
expect(screen.getByRole('checkbox')).toHaveAttribute('data-testid', 'custom-option')
})

type CreateWrapperProps = Omit<ComponentProps<typeof ChipSelectContextProvider>, 'children'>

function createWrapper(contextProps: CreateWrapperProps) {
function createWrapper(context: ChipSelectContext.Value) {
return function Wrapper({ children }: { children: ReactNode }) {
return <ChipSelectContextProvider {...contextProps}>{children}</ChipSelectContextProvider>
return <ChipSelectContext.Provider value={context}>{children}</ChipSelectContext.Provider>
}
}
6 changes: 2 additions & 4 deletions src/core/chip-select/chip-select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChipSelectContext, ChipSelectContextProvider, useChipSelectContext } from './context'
import { ChipSelectContext, useChipSelectContext } from './context'
import { ChipSelectOption } from './chip-select-option'
import { determineNextControlledState } from './chip'
import { ElChipSelect } from './styles'
Expand Down Expand Up @@ -59,9 +59,7 @@ export function ChipSelect({
}: ChipSelect.Props) {
return (
<ElChipSelect {...rest} data-flow={flow} data-overflow={overflow}>
<ChipSelectContextProvider form={form} multiple={multiple} name={name} size={size}>
{children}
</ChipSelectContextProvider>
<ChipSelectContext.Provider value={{ form, multiple, name, size }}>{children}</ChipSelectContext.Provider>
</ElChipSelect>
)
}
Expand Down
Loading
Loading