Skip to content
Draft
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
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
React component library for [Cloudscape Design System](https://cloudscape.design/) — an open source design system for building accessible, inclusive web experiences at scale.

To install dependencies use `npm install`
Build with `npm run quick-build` for dev, or `npm run build` for a full production build.

For component structure, props, events, and refs, see docs/COMPONENT_CONVENTIONS.md.
For design tokens, CSS rules, and RTL support, see docs/STYLING.md.
For test commands, see docs/TESTING.md.
For writing tests and test utils, see docs/WRITING_TESTS.md.
For prettier, stylelint, and eslint (`npm run lint`), see docs/CODE_STYLE.md.
For dev/test pages, see docs/DEV_PAGES.md.
For API documentation comments, see docs/API_DOCS.md.
For internal shared utilities, see docs/INTERNALS.md.
1 change: 1 addition & 0 deletions build-tools/utils/pluralize.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const pluralizationMap = {
Pagination: 'Paginations',
AppLayoutToolbar: 'AppLayoutToolbars',
PanelLayout: 'PanelLayouts',
NavigationBar: 'NavigationBars',
PieChart: 'PieCharts',
Popover: 'Popovers',
ProgressBar: 'ProgressBars',
Expand Down
10 changes: 10 additions & 0 deletions docs/API_DOCS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# API Documentation Comments

This project uses `@cloudscape-design/documenter` to generate API docs from JSDoc comments in component interface files (`src/<component>/interfaces.ts`).

## Special tags

- `@i18n` — marks internationalization properties
- `@analytics` — marks analytics metadata properties
- `@deprecated` — marks deprecated properties (include replacement info)
- `@displayname` — overrides the display name (e.g. `children` → `text`)
18 changes: 18 additions & 0 deletions docs/CODE_STYLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Code Style

This project enforces formatting and linting via prettier, stylelint, and eslint. Run `npm run lint` to check everything.

Before writing or modifying code, read the relevant config files:

- Prettier: `.prettierrc`
- Stylelint: `.stylelintrc`
- ESLint: `eslint.config.mjs`

Follow whatever rules are defined there. Do not guess — read the configs.

All source files must include the license header:

```
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
```
65 changes: 65 additions & 0 deletions docs/COMPONENT_CONVENTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Component Conventions

## Ground Rules

- Always read existing code for the component you're working on before writing new code. For new components, pick a similar one as your template.
- Inner HTML structure and class names are not public API — don't rely on them.

## Component Structure

Each public component lives in `src/<component-name>/index.tsx` and must:

- Assign default prop values in the destructuring signature (not `defaultProps`), then pass them explicitly to the internal component
- Call `useBaseComponent` for telemetry — pass `props` (primitive/enum values with defaults applied) and optionally `metadata` (derived counters or booleans). No state props, no PII, no user input strings.
- Use `getExternalProps` or `getBaseProps` to strip internal `__`-prefixed props
- Call `applyDisplayName` at the bottom of the file
- Export the props interface as `${ComponentName}Props`

Each public component has a private counterpart at `src/<component-name>/internal.tsx` for composition. Internal props are prefixed with `__`. The public component must not add behavior beyond what the internal component provides.

Shared hooks, components, contexts, and helpers live in `src/internal/`. Always check there before building new shared code.

## Props & Interfaces

- Props interface: `${ComponentName}Props`, namespace sub-types under it (e.g. `${ComponentName}Props.Variant`)
- Union types must be type aliases (no inline unions)
- Array types must use `ReadonlyArray<T>`
- Cast string props to string at runtime if rendered in JSX — React accepts JSX content there
- Component return type must be exactly `JSX.Element` — `null` or arrays break the doc generator
- Components exported as default from `index.tsx` are public; everything else is private

## Events

- Use `CancelableEventHandler<DetailType>` or `NonCancelableEventHandler<DetailType>` from `../internal/events`
- All `on*` props must use these interfaces (build fails otherwise)

## Refs

- Expose via `${ComponentName}Props.Ref` interface
- Use `useForwardFocus(ref, elementRef)` for simple focus delegation
- For `React.forwardRef` generics, create a `${ComponentName}ForwardRefType` interface

## Controllable Components

- Controlled: requires `value` + `onChange` (e.g. Input)
- Uncontrolled: internal state only (e.g. dropdown open)
- Controllable: uncontrolled by default, controlled when `value` is set

Use `useControllable` — read existing components for the pattern.

## Element Queries

Use `useContainerBreakpoints` instead of CSS media queries. Handle `null` on first render.

## I18n

Three string categories:
- Static → `i18nStrings` property
- Dynamic → functions in `i18nStrings` returning strings
- Context-dependent → top-level props (out of scope for i18n provider)

Use `useInternalI18n('<component>')` to consume. Test with `TestI18nProvider` from `src/i18n/testing`.

## Dependencies

Before adding any dependency: must support React 16.8+ and latest 3 major Chrome/Firefox/Edge, no global state, ESM preferred, no external resources (CSP).
11 changes: 11 additions & 0 deletions docs/DEV_PAGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Dev Pages

Dev and test pages live in `pages/<component-name>/`. They are used for local development, integration tests, and visual regression tests.

## Rules

- Export a default function component
- Import components via `~components/<name>` (not relative paths to `src/`)
- For visual regression content, either use `SimplePage` from `pages/app/templates` (handles heading, screenshot area, i18n, and layout) or wrap content in `ScreenshotArea` from `pages/utils/screenshot-area` with a manual `<h1>`
- Use `createPermutations` and `PermutationsView` from `pages/utils/` for permutation pages
- If multiple pages share data (like `i18nStrings`), put it in a `common.tsx` in the same directory
16 changes: 16 additions & 0 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Internal Utilities

Shared infrastructure lives in `src/internal/`.

## What's in there

- `hooks/` — shared React hooks (component telemetry, controllable state, focus management, container/element queries, visual mode detection, intersection observers, debounce/throttle, etc.)
- `components/` — shared internal UI building blocks (dropdowns, options, focus locks, tooltips, chart primitives, transitions, screen-reader utilities, drag handles, etc.)
- `context/` — React contexts for cross-component communication (form fields, modals, split panels, container headers, etc.)
- `utils/` — pure helper functions (DOM helpers, key handling, date/time, locale, display names, prop stripping, etc.)
- `events/` — event handler type definitions used by all component `on*` props
- `base-component/` — base prop extraction utilities
- `styles/` — shared SCSS mixins
- `analytics/` — telemetry and funnel metrics

Always check `src/internal/` before introducing new utilities, hooks, or shared components.
26 changes: 26 additions & 0 deletions docs/STYLING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Styling

Never hardcode CSS spacing, colors, borders, shadows, typography, or motion values. This project uses design tokens and custom CSS properties.

## Design Tokens

Tokens are defined in `style-dictionary/` and consumed in SCSS via the `awsui` namespace.

## Custom CSS Properties

Custom CSS properties are defined in `build-tools/utils/custom-css-properties.js`. Read this file to see what's available.

## Rules

- Apply `styles-reset` mixin on every root element — prevents parent styles from leaking in
- Root elements must not have outer margins — spacing is managed by parent components
- No descendant combinators (`.a .b` with a space) — breaks CSS scoping
- Wrap all animations in the `with-motion` mixin — ensures motion can be toggled
- Use logical properties only — no `left`/`right`/`top`/`bottom`/`width`/`height` in CSS. Use `inline-start`/`inline-end`/`block-start`/`block-end`/`inline-size`/`block-size` instead. This is required for RTL support.

## RTL Support

For bidirectional layout support, use the internal RTL utilities in `src/internal/`:

- SCSS: `with-direction` mixin for direction-aware styles
- JS: `getIsRtl` for detecting direction, `handleKey` for direction-aware key handling, `getLogicalBoundingClientRect` / `getOffsetInlineStart` / `getScrollInlineStart` for direction-aware measurements
37 changes: 37 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Testing

## Quick Reference

```
npm test # all tests (unit + integ + a11y)
npm run test:unit # unit + build-tool tests
npm run test:integ # integration tests (starts dev server automatically)
npm run test:motion # motion tests (starts dev server automatically)
npm run test:a11y # accessibility tests
```

The npm scripts use gulp tasks that handle env vars (`TZ=UTC`, `NODE_OPTIONS=--experimental-vm-modules`) and dev server lifecycle automatically.

## Targeting Specific Files

For running a specific test file or folder, call jest directly with the appropriate config:

```
# Unit
TZ=UTC node_modules/.bin/jest -c jest.unit.config.js src/button/__tests__/button.test.tsx

# Integration (requires dev server running via `npm start`)
NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.integ.config.js src/input/__integ__/

# Motion (requires dev server running via `npm start`)
NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.motion.config.js src/flashbar/__motion__/

# Build-tool / stylelint
NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.build-tools.config.js build-tools/stylelint
```

## Updating Snapshots

```
TZ=UTC node_modules/.bin/jest -u -c jest.unit.config.js src/__tests__/snapshot-tests/
```
31 changes: 31 additions & 0 deletions docs/WRITING_TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Writing Tests

## Test Utils

Use the project's test utils in `src/test-utils/` — don't query the DOM directly.

- `src/test-utils/dom/` — unit test wrappers (JSDOM)
- `src/test-utils/selectors/` — integration test wrappers (real browser)

Read the wrapper for the component you're testing to see available `find*` methods.

### Authoring Test Utils

Each wrapper extends `ComponentWrapper`, has a static `rootSelector`, and explicit return types on all methods. Test-util CSS classes go in `src/<component-name>/test-classes/styles.scss`. Read existing wrappers for the pattern.

## Unit Tests

Use `react-testing-library` to render, combined with test-utils. Prefer test-utils for public interactions, react-testing-library for internal corner cases. Read an existing test file for the setup pattern.

## Integration Tests

Use `useBrowser` from `@amzn/awsui-browser-test-tools/use-browser`. Use a `setupTest` wrapper with `waitForVisible`. Don't wait on tag name selectors — they're visible before JS loads. Multiple assertions per test are fine (e2e tests are slow). Use page object pattern for files with many tests.

## I18n Testing

Test `useInternalI18n` with `TestI18nProvider` from `src/i18n/testing`. Read existing i18n tests for the pattern.

## Gotchas

- `findAll`/`findAllByClassName` with `.get()` uses `:nth-child()` — only works if items share the same parent node
- All dev pages are axe-checked automatically. A11y violations fail the build. Checks run in dark mode only.
74 changes: 74 additions & 0 deletions pages/navigation-bar/simple.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import ButtonGroup from '~components/button-group';
import NavigationBar from '~components/navigation-bar';

import ScreenshotArea from '../utils/screenshot-area';

export default function NavigationBarPage() {
return (
<main>
<h1>Navigation Bar</h1>
<ScreenshotArea>
<h2>Primary — horizontal (default)</h2>
<NavigationBar ariaLabel="Main navigation">
<span style={{ color: 'white', fontWeight: 'bold' }}>My Product</span>
<div style={{ marginInlineStart: 'auto' }}>
<ButtonGroup
variant="icon"
ariaLabel="Utilities"
items={[
{ type: 'icon-button', id: 'settings', iconName: 'settings', text: 'Settings' },
{ type: 'icon-button', id: 'user', iconName: 'user-profile', text: 'User' },
]}
/>
</div>
</NavigationBar>

<h2>Secondary — horizontal</h2>
<NavigationBar variant="secondary" ariaLabel="Page toolbar">
<span>Breadcrumbs / Page title</span>
<div style={{ marginInlineStart: 'auto' }}>
<ButtonGroup
variant="icon"
ariaLabel="Actions"
items={[{ type: 'icon-button', id: 'edit', iconName: 'edit', text: 'Edit' }]}
/>
</div>
</NavigationBar>

<h2>Primary — vertical</h2>
<div style={{ display: 'flex', blockSize: '200px' }}>
<NavigationBar variant="primary" placement="vertical" ariaLabel="Side navigation">
<ButtonGroup
variant="icon"
ariaLabel="Navigation"
items={[
{ type: 'icon-button', id: 'home', iconName: 'angle-right', text: 'Home' },
{ type: 'icon-button', id: 'settings', iconName: 'settings', text: 'Settings' },
]}
/>
</NavigationBar>
<div style={{ padding: '16px' }}>Main content</div>
</div>

<h2>Secondary — vertical</h2>
<div style={{ display: 'flex', blockSize: '200px' }}>
<NavigationBar variant="secondary" placement="vertical" ariaLabel="Side toolbar">
<ButtonGroup
variant="icon"
ariaLabel="Tools"
items={[
{ type: 'icon-button', id: 'search', iconName: 'search', text: 'Search' },
{ type: 'icon-button', id: 'filter', iconName: 'filter', text: 'Filter' },
]}
/>
</NavigationBar>
<div style={{ padding: '16px' }}>Main content</div>
</div>
</ScreenshotArea>
</main>
);
}
34 changes: 34 additions & 0 deletions src/navigation-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
'use client';
import React from 'react';

import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { getExternalProps } from '../internal/utils/external-props';
import { NavigationBarProps } from './interfaces';
import InternalNavigationBar from './internal';

export { NavigationBarProps };

const NavigationBar = React.forwardRef<NavigationBarProps.Ref, NavigationBarProps>(function NavigationBar(
{ variant = 'primary', placement = 'horizontal', ...props },
ref
) {
const baseComponentProps = useBaseComponent('NavigationBar', {
props: { variant, placement, disableBorder: props.disableBorder },
});
return (
<InternalNavigationBar
ref={ref}
variant={variant}
placement={placement}
{...getExternalProps(props)}
{...baseComponentProps}
/>
);
});

export default NavigationBar;

applyDisplayName(NavigationBar, 'NavigationBar');
Loading
Loading