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: 0 additions & 1 deletion jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ const customJestConfig = {
'/node_modules/(?!.*(wagmi|viem|@wagmi|@viem|@walletconnect|@metamask|@coinbase|@radix-ui|@storybook))'
],
testPathIgnorePatterns: [
'<rootDir>/src/components/responsive/__tests__/ResponsiveContainer.test.tsx',
'<rootDir>/src/lib/__tests__/mobile-optimizer.test.ts',
'<rootDir>/src/lib/__tests__/verify-performance-monitoring.ts'
],
Expand Down
153 changes: 135 additions & 18 deletions src/components/responsive/ResponsiveContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
/**
* ResponsiveContainer Component
*
*
* A container component with responsive padding that scales with viewport size.
* Provides consistent padding across all pages with fluid scaling between breakpoints.
*
*
* Accessibility (WCAG 2.1):
* - Supports polymorphic `as` prop so consumers can render semantic HTML elements
* (main, section, article, aside, header, footer, nav) instead of a plain <div>.
* - Accepts `aria-label`, `aria-labelledby`, and `aria-describedby` to give
* screen readers a meaningful context for landmark / sectioning elements.
* - Accepts `role` so consumers can assign explicit ARIA landmark roles when a
* native semantic element is not appropriate.
* - Forwards all standard HTML div attributes so keyboard-navigation props
* (tabIndex, onKeyDown, etc.) flow through without extra wrapping.
* - The `data-testid` attribute defaults to `"responsive-container"` to make
* the element easily targetable in automated accessibility tests.
*
* Features:
* - Responsive padding: 16px (mobile) → 24px (tablet) → 32px (desktop)
* - Fluid scaling between breakpoints using CSS clamp()
* - Uses Viewport Provider for viewport detection
* - Supports custom className for additional styling
*
*
* Requirements: 3.1, 3.2, 8.5
*/

Expand All @@ -19,27 +31,91 @@ import React from 'react';
import { useViewport } from '@/providers/ViewportProvider';
import { cn } from '@/lib/utils';

export interface ResponsiveContainerProps {
// ---------------------------------------------------------------------------
// Allowed semantic element types for the polymorphic `as` prop
// ---------------------------------------------------------------------------
type AllowedElement =
| 'div'
| 'main'
| 'section'
| 'article'
| 'aside'
| 'header'
| 'footer'
| 'nav';

export interface ResponsiveContainerProps
extends React.HTMLAttributes<HTMLElement> {
children: React.ReactNode;
className?: string;

/**
* Render as a semantic HTML element instead of a plain <div>.
* Defaults to "div".
*
* @example
* <ResponsiveContainer as="main" aria-label="Main content">
* ...
* </ResponsiveContainer>
*/
as?: AllowedElement;

/**
* ARIA role override. Useful when `as="div"` but a landmark role is needed.
* Prefer using the `as` prop with a native semantic element where possible,
* as that provides better screen-reader compatibility.
*/
role?: React.AriaRole;

/**
* Accessible label for this container region.
* Required (or `aria-labelledby`) when the container is rendered as a
* landmark element (main, nav, section, aside, etc.) so that screen readers
* can distinguish multiple regions of the same type.
*/
'aria-label'?: string;

/** References the id of an element that labels this container region. */
'aria-labelledby'?: string;

/** References the id of an element that describes this container region. */
'aria-describedby'?: string;

/** data-testid for automated testing. Defaults to "responsive-container". */
'data-testid'?: string;
}

/**
* ResponsiveContainer provides consistent, viewport-aware padding
*
*
* Padding scales:
* - Mobile (<768px): 16px
* - Tablet (768-1024px): 24px (fluid scaling)
* - Desktop (≥1024px): 32px
*
* @example
*
* @example Basic usage
* ```tsx
* <ResponsiveContainer>
* <h1>Page Content</h1>
* <p>This content has responsive padding</p>
* </ResponsiveContainer>
* ```
*
*
* @example Semantic landmark — accessible main content region
* ```tsx
* <ResponsiveContainer as="main" aria-label="Main content">
* <h1>Welcome</h1>
* </ResponsiveContainer>
* ```
*
* @example Navigation region
* ```tsx
* <ResponsiveContainer as="nav" aria-label="Primary navigation">
* <a href="/home">Home</a>
* <a href="/properties">Properties</a>
* </ResponsiveContainer>
* ```
*
* @example With custom className
* ```tsx
* <ResponsiveContainer className="bg-gray-100">
Expand All @@ -50,11 +126,13 @@ export interface ResponsiveContainerProps {
export const ResponsiveContainer: React.FC<ResponsiveContainerProps> = ({
children,
className,
as: Element = 'div',
'data-testid': testId = 'responsive-container',
...rest
}) => {
const { category } = useViewport();

// Calculate padding based on viewport category
// Using inline styles for precise control, but could also use Tailwind classes
const getPadding = (): string => {
switch (category) {
case 'mobile':
Expand All @@ -69,43 +147,82 @@ export const ResponsiveContainer: React.FC<ResponsiveContainerProps> = ({
};

return (
<div
<Element
className={cn('responsive-container', className)}
style={{
padding: getPadding(),
// Ensure no horizontal overflow (Requirement 3.1)
maxWidth: '100%',
boxSizing: 'border-box',
}}
data-testid={testId}
{...rest}
>
{children}
</div>
</Element>
);
};

// ---------------------------------------------------------------------------
// Fluid variant
// ---------------------------------------------------------------------------

export interface ResponsiveContainerFluidProps
extends React.HTMLAttributes<HTMLElement> {
children: React.ReactNode;
className?: string;

/**
* Render as a semantic HTML element. Defaults to "div".
*/
as?: AllowedElement;

role?: React.AriaRole;
'aria-label'?: string;
'aria-labelledby'?: string;
'aria-describedby'?: string;

/** data-testid for automated testing. Defaults to "responsive-container-fluid". */
'data-testid'?: string;
}

/**
* Alternative implementation using CSS clamp() for fluid scaling
* This version provides smooth scaling between breakpoints
* Alternative implementation using CSS clamp() for fluid scaling.
* This version provides smooth scaling between breakpoints.
*
* @example
* ```tsx
* <ResponsiveContainerFluid as="section" aria-labelledby="section-title">
* <h2 id="section-title">Section Heading</h2>
* <p>Content with fluid responsive padding</p>
* </ResponsiveContainerFluid>
* ```
*/
export const ResponsiveContainerFluid: React.FC<ResponsiveContainerProps> = ({
export const ResponsiveContainerFluid: React.FC<
ResponsiveContainerFluidProps
> = ({
children,
className,
as: Element = 'div',
'data-testid': testId = 'responsive-container-fluid',
...rest
}) => {
return (
<div
<Element
className={cn('responsive-container-fluid', className)}
style={{
// Fluid padding using clamp()
// Formula: clamp(min, preferred, max)
// Preferred value scales linearly: min + (max - min) * (viewport - minViewport) / (maxViewport - minViewport)
// Using CSS calc with viewport units for automatic scaling
// Scales linearly from 16px at narrow viewports to 32px at wide viewports
padding: 'clamp(16px, 4vw, 32px)',
maxWidth: '100%',
boxSizing: 'border-box',
}}
data-testid={testId}
{...rest}
>
{children}
</div>
</Element>
);
};

Expand Down
Loading