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
98 changes: 98 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## About Octuple

Octuple is Eightfold's React Design System Component Library. It's a comprehensive collection of reusable React components, utilities, and hooks built with TypeScript and SCSS modules.

## Development Commands

### Primary Development Commands
- `yarn storybook` - Run Storybook development server on port 2022
- `yarn build` - Build the library for production (runs lint + rollup build)
- `yarn test` - Run Jest unit tests with coverage
- `yarn lint` - Run ESLint on all JS/JSX/TS/TSX files
- `yarn typecheck` - Run TypeScript type checking without emitting files

### Testing Commands
- `yarn test:update` - Update Jest snapshots
- Run single test: `jest path/to/test.test.tsx`

### Build Commands
- `yarn build-storybook` - Build Storybook for deployment
- `yarn build:webpack` - Alternative webpack-based build (runs lint + webpack)

### Release Commands
- `yarn release` - Standard version release (skips tests)
- `yarn release:minor` - Minor version release
- `yarn release:patch` - Patch version release
- `yarn release:major` - Major version release

## Code Architecture

### Component Structure
Components follow a strict modular structure in `src/components/`:
- Each component has its own directory with TypeScript files, SCSS modules, Storybook stories, and Jest tests
- Main export file: `src/octuple.ts` - exports all public components and utilities
- Locale exports: `src/locale.ts` - internationalization utilities

### Key Directories
- `src/components/` - All React components organized by component name
- `src/hooks/` - Custom React hooks (useBoolean, useGestures, useMatchMedia, etc.)
- `src/shared/` - Shared utilities and common components (FocusTrap, ResizeObserver, utilities)
- `src/styles/` - Global SCSS styles and variables
- `src/tests/` - Test utilities and setup files

### Component Patterns
Components follow consistent patterns:
- TypeScript interfaces defined in `ComponentName.types.ts`
- SCSS modules using kebab-case class names (referenced as camelCase in JS)
- Exported through barrel exports in `index.ts` files
- Use `mergeClasses` utility for conditional class name handling
- Support for themes via ConfigProvider context

### Build System
- **Rollup** for library bundling (primary build system)
- **Webpack** alternative build available
- **SCSS modules** with camelCase conversion
- **TypeScript** compilation with strict type checking
- **PostCSS** for CSS processing and minification
- Outputs both ESM (.mjs) and CommonJS (.js) formats

### Testing Approach
- **Jest** with React Testing Library
- **Enzyme** with React 17 adapter
- **Snapshot testing** for component rendering
- **MatchMedia mock** for responsive testing
- **ResizeObserver** polyfill for tests
- Coverage collection configured

### Component Guidelines
Follow the established patterns in `src/components/COMPONENTS.md`:
- Use functional components with TypeScript
- Define props interfaces with JSDoc comments
- Use SCSS modules for styling
- Include Storybook stories for documentation
- Write comprehensive Jest tests with snapshots
- Export all public APIs through barrel exports

### Storybook
- Development server runs on port 2022
- Stories follow the pattern `ComponentName.stories.tsx`
- Used for component documentation and visual testing

### Key Dependencies
- React 17+ (peer dependency)
- TypeScript for type safety
- SCSS for styling with CSS modules
- Storybook for component documentation
- Jest + React Testing Library for testing
- Various UI utility libraries (@floating-ui/react, react-spring, etc.)

### Conventional Commits
Commit messages must follow the Conventional Commits specification:
- Format: `<type>[optional scope]: <description>`
- Types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
- Subject line max 100 characters
- Combined body and footer max 100 characters
108 changes: 108 additions & 0 deletions src/components/DateTimePicker/DatePicker/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,110 @@ const Range_Status_Story: ComponentStory<typeof RangePicker> = (args) => {
);
};

const Accessibility_Announcement_Story: ComponentStory<typeof DatePicker> = (
args
) => {
const onChange: DatePickerProps['onChange'] = (date, dateString) => {
console.log(date, dateString);
};

return (
<ConfigProvider themeOptions={{ name: 'blue' }}>
<Stack direction="vertical" flexGap="xl">
<div>
<h3>Default Announcement (uses locale text)</h3>
<p>Opens with "Use arrow keys to navigate dates" announcement</p>
<Stack direction="vertical" flexGap="m">
<DatePicker
{...args}
onChange={onChange}
announceArrowKeyNavigation
placeholder="Click to open with announcement"
/>
<DatePicker.RangePicker
onChange={(values, formatString) => {
console.log(values, formatString);
}}
trapFocus
announceArrowKeyNavigation
/>
</Stack>
</div>

<div>
<h3>Custom Announcement Message</h3>
<p>Opens with custom announcement text</p>
<DatePicker
{...args}
onChange={onChange}
announceArrowKeyNavigation="Navigate this calendar using your arrow keys"
placeholder="Click to open with custom announcement"
/>
</div>

<div>
<h3>Focus Trap + Announcement (Coordinated)</h3>
<p>
Announces navigation first, then automatically moves focus to
calendar after 1 second
</p>
<DatePicker
{...args}
onChange={onChange}
announceArrowKeyNavigation={true}
trapFocus={true}
placeholder="Click for announcement → auto focus shift"
/>
</div>

<div>
<h3>Focus Trap Only (Immediate)</h3>
<p>Immediately moves focus to calendar without announcement</p>
<DatePicker
{...args}
onChange={onChange}
trapFocus={true}
placeholder="Click for immediate focus shift"
/>
</div>

<div>
<h3>No Announcement (default behavior)</h3>
<p>Opens without any navigation announcement or focus changes</p>
<DatePicker
{...args}
onChange={onChange}
placeholder="Click to open without announcement"
/>
</div>

<div
style={{
backgroundColor: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
}}
>
<h4>Screen Reader Instructions:</h4>
<p>
To test this feature with a screen reader:
<br />• Enable your screen reader (NVDA, JAWS, VoiceOver, etc.)
<br />• <strong>Coordinated example:</strong> Click "announcement →
auto focus shift" - hear announcement, then focus moves to calendar
after 1 second
<br />• <strong>Immediate example:</strong> Click "immediate focus
shift" - focus moves to calendar immediately
<br />• <strong>Keyboard navigation:</strong> Use TAB/Shift+TAB to
cycle within the trapped focus area
<br />• <strong>Exit:</strong> Press ESC or click outside to return
focus to input and close picker
</p>
</div>
</Stack>
</ConfigProvider>
);
};

const Range_Picker_With_Aria_Labels_Story: ComponentStory<
typeof RangePicker
> = (args) => <DatePicker.RangePicker {...args} />;
Expand Down Expand Up @@ -592,6 +696,9 @@ export const Single_Borderless = Single_Borderless_Story.bind({});
export const Range_Borderless = Range_Borderless_Story.bind({});
export const Single_Status = Single_Status_Story.bind({});
export const Range_Status = Range_Status_Story.bind({});
export const Accessibility_Announcement = Accessibility_Announcement_Story.bind(
{}
);
export const Range_Picker_With_Aria_Labels =
Range_Picker_With_Aria_Labels_Story.bind({});

Expand Down Expand Up @@ -622,6 +729,7 @@ export const __namedExportsOrder = [
'Range_Borderless',
'Single_Status',
'Range_Status',
'Accessibility_Announcement',
'Range_Picker_With_Aria_Labels',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default function generateRangePicker<DateType>(
todayButtonProps,
todayActive = false,
todayText: defaultTodayText,
trapFocus = false,
...rest
} = props;
const largeScreenActive: boolean = useMatchMedia(Breakpoints.Large);
Expand Down Expand Up @@ -268,6 +269,7 @@ export default function generateRangePicker<DateType>(
superPrevIcon={IconName.mdiChevronDoubleLeft}
superNextIcon={IconName.mdiChevronDoubleRight}
allowClear
trapFocus={trapFocus}
{...rest}
{...additionalOverrideProps}
classNames={mergeClasses([
Expand Down
13 changes: 13 additions & 0 deletions src/components/DateTimePicker/DatePicker/Styles/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,16 @@ $picker-input-padding-vertical: max(
cursor: not-allowed;
opacity: $disabled-alpha-value;
}

// Screen reader only content mixin
@mixin screen-reader-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
6 changes: 2 additions & 4 deletions src/components/DateTimePicker/Internal/Hooks/useCellProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { formatValue } from '../Utils/dateUtil';
import type { GenerateConfig } from '../Generate';
import type { NullableDateType, Locale, RangeValue } from '../OcPicker.types';
import type { NullableDateType, Locale } from '../OcPicker.types';

type UseCellPropsArgs<DateType> = {
generateConfig: GenerateConfig<DateType>;
Expand All @@ -11,7 +11,6 @@ type UseCellPropsArgs<DateType> = {
) => boolean;
today?: NullableDateType<DateType>;
locale: Locale;
rangedValue?: RangeValue<DateType>;
};

export default function useCellProps<DateType>({
Expand All @@ -20,7 +19,6 @@ export default function useCellProps<DateType>({
isSameCell,
locale,
generateConfig,
rangedValue,
}: UseCellPropsArgs<DateType>) {
function getCellProps(currentDate: DateType) {
return {
Expand All @@ -38,5 +36,5 @@ export default function useCellProps<DateType>({
isCellFocused: isSameCell(value, currentDate),
};
}
return rangedValue ? undefined : getCellProps;
return getCellProps;
}
1 change: 1 addition & 0 deletions src/components/DateTimePicker/Internal/Locale/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const locale: Locale = {
nextAriaLabel: 'Next year',
superPrevAriaLabel: 'Previous year',
superNextAriaLabel: 'Next year',
arrowKeyNavigationText: 'Use arrow keys to navigate the calendar',
};

export default locale;
17 changes: 16 additions & 1 deletion src/components/DateTimePicker/Internal/OcPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type MergedOcPickerProps<DateType> = {
function InnerPicker<DateType>(props: OcPickerProps<DateType>) {
const {
allowClear,
announceArrowKeyNavigation,
autoComplete = 'off',
autoFocus,
bordered = true,
Expand Down Expand Up @@ -370,6 +371,16 @@ function InnerPicker<DateType>(props: OcPickerProps<DateType>) {
partialNode = partialRender(partialNode);
}

const navigationAnnouncement = announceArrowKeyNavigation ? (
<div
className={styles.srOnly}
aria-live="polite"
aria-atomic="true"
>
{announceArrowKeyNavigation === true ? locale?.arrowKeyNavigationText : announceArrowKeyNavigation}
</div>
) : null;

const partial: JSX.Element = trapFocus ? (
<FocusTrap
data-testid="picker-dialog"
Expand All @@ -388,7 +399,10 @@ function InnerPicker<DateType>(props: OcPickerProps<DateType>) {
}
}}
>
{partialNode}
<>
{navigationAnnouncement}
{partialNode}
</>
</FocusTrap>
) : (
<div
Expand All @@ -397,6 +411,7 @@ function InnerPicker<DateType>(props: OcPickerProps<DateType>) {
e.preventDefault();
}}
>
{navigationAnnouncement}
{partialNode}
</div>
);
Expand Down
10 changes: 10 additions & 0 deletions src/components/DateTimePicker/Internal/OcPicker.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export type Locale = {
* The super next aria label.
*/
superNextAriaLabel?: string;
/**
* The arrow key navigation announcement text.
*/
arrowKeyNavigationText?: string;
};

export type PartialMode =
Expand Down Expand Up @@ -621,6 +625,12 @@ export type OcPickerSharedProps<DateType> = {
* @default false
*/
autoFocus?: boolean;
/**
* Announces arrow key navigation instructions when the picker opens.
* When true, uses default locale text. When string, uses custom message.
* @default false
*/
announceArrowKeyNavigation?: boolean | string;
/**
* Determines if the picker has a border style.
*/
Expand Down
1 change: 0 additions & 1 deletion src/components/DateTimePicker/Internal/OcPickerPartial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@ function OcPickerPartial<DateType>(props: OcPickerPartialProps<DateType>) {
}
return partialRef.current?.onKeyDown(e);
}

return null;
};

Expand Down
Loading