Skip to content

Stability audit: OneCalendar / OneEmptyState #3888

Description

@eliseo-juan

Component Group: OneCalendar / OneEmptyState

Sequential stability audit — part of the component-by-component quality sweep.

Reviewers: Code Review · A11y · Storybook · Test Coverage


BLOCKING

Severity Area File:line Description Suggested fix
BLOCKING Import OneCalendar/OneCalendar.test.tsx:1 import React from "react" — default import is forbidden Remove entirely (React 17+ JSX transform)
BLOCKING Folder structure OneCalendar/OneCalendar.test.tsx Test file is at component root, not inside __tests__/ subfolder Move to OneCalendar/__tests__/OneCalendar.test.tsx
BLOCKING Folder structure OneCalendar/OneCalendar.stories.tsx Story file is at component root, not inside __stories__/ subfolder Move to OneCalendar/__stories__/OneCalendar.stories.tsx
BLOCKING Story OneCalendar/OneCalendar.stories.tsx No Snapshot story with withSnapshot({}) for Chromatic Add export const Snapshot: Story = { parameters: withSnapshot({}) }
BLOCKING Story tags OneCalendar/OneCalendar.stories.tsx:24 tags: ["autodocs"] missing stability tag Add "stable" or "experimental"
BLOCKING displayName OneCalendar/OneCalendar.tsx OneCalendarInternal function component has no displayName Add OneCalendarInternal.displayName = "OneCalendarInternal"
BLOCKING Encapsulation OneCalendar/OneCalendar.tsx OneCalendarInternal exported publicly — internal implementation detail leaked Export from a separate internal entry point, not the main public file
BLOCKING Internal type leak OneCalendar/OneCalendar.stories.tsx:65 type OneCalendarInternalStory = StoryObj<Meta<typeof OneCalendarInternal>> wraps private OneCalendarInternal in the public story file Move compact/internal stories to a separate __stories__/OneCalendarInternal.stories.tsx
BLOCKING Animation / Reduced Motion granularities/day/DayView.tsx:1 AnimatePresence + motion.div with duration: 0.15 and x-axis translation — no useReducedMotion() guard Import useReducedMotion from motion/react; set duration: 0 and skip translate variants when true
BLOCKING Animation / Reduced Motion granularities/month/MonthView.tsx:1 Same — motion.div slide animation with duration: 0.15, no useReducedMotion() check Same fix
BLOCKING Animation / Reduced Motion granularities/year/YearView.tsx:1 AnimatePresence + motion.div, no useReducedMotion() import or check Same fix
BLOCKING Animation / Reduced Motion granularities/week/WeekView.tsx:1 motion.div with duration: compact ? 0.1 : 0.15, no reduced-motion guard Same fix
BLOCKING Animation / Reduced Motion granularities/quarter/QuarterView.tsx:1 AnimatePresence + motion.div, no useReducedMotion() guard Same fix
BLOCKING Animation / Reduced Motion granularities/halfyear/HalfyearView.tsx:1 AnimatePresence + motion.div, no useReducedMotion() guard Same fix
BLOCKING Form semantics granularities/quarter/QuarterView.tsx Quarter <button> elements missing type="button" — will submit ancestor forms Add type="button" to every quarter button
BLOCKING Form semantics granularities/halfyear/HalfyearView.tsx Halfyear <button> elements missing type="button" Add type="button" to every halfyear button
BLOCKING Form semantics granularities/year/YearView.tsx Year <button> elements missing type="button" Add type="button" to every year button
BLOCKING Tests missing OneCalendar/granularities/range/ No test file for range granularity — range/index.tsx has zero coverage Create range/__tests__/index.test.tsx covering toRange, toRangeString, toString, fromString, navigate, getPrevNext
BLOCKING Story type OneEmptyState/__stories__/OneEmptyState.stories.tsx:20 type Story = StoryObj<typeof OneEmptyState> — must be StoryObj<typeof meta> Change to type Story = StoryObj<typeof meta>
BLOCKING Story OneEmptyState/__stories__/OneEmptyState.stories.tsx No Snapshot story with withSnapshot({}) Add export const Snapshot: Story = { parameters: withSnapshot({}) }
BLOCKING displayName OneEmptyState/OneEmptyState.tsx _OneEmptyState has no displayName Add _OneEmptyState.displayName = "OneEmptyState"
BLOCKING Tests missing OneEmptyState/ No __tests__/ directory — entire component is untested Create OneEmptyState/__tests__/OneEmptyState.test.tsx with render, variant, action, and dataTestId tests
BLOCKING Direct lucide-react import OneEmptyState/__stories__/OneEmptyState.stories.tsx:3 import { Plus } from "lucide-react" — must use @/icons/app wrapper Replace with import { Plus } from "@/icons/app"

SUGGESTION

Severity Area File:line Description Suggested fix
SUGGESTION Naming OneCalendar/OneCalendar.tsx One* prefix is legacy naming Consider renaming to F0Calendar at a breaking-change window
SUGGESTION Naming OneEmptyState/OneEmptyState.tsx One* prefix is legacy naming Consider renaming to F0EmptyState at a breaking-change window
SUGGESTION Types OneCalendar/types.ts CalendarView, CalendarMode, and other public types use type — prefer interface for public prop shapes Change to interface
SUGGESTION Types OneEmptyState/types.ts ActionProps, OneEmptyStateProps use type — prefer interface Change to interface
SUGGESTION ArgTypes OneCalendar/OneCalendar.stories.tsx:37 options: [0,1,2,3,4,5,6] hardcoded instead of referencing exported WeekStartDay Use Object.values(WeekStartDay)
SUGGESTION Story API OneCalendar/OneCalendar.stories.tsx:619–772 CompactMonthSingle, CompactMonthRange, CompactWeek, CompactDayRange, and RegularVsCompact render <OneCalendarInternal> directly, bypassing the public API Either expose compact as a public prop, or move these to a private story file
SUGGESTION Story metadata OneCalendar/OneCalendar.stories.tsx defaultMonth not set in several stories — calendar may show unexpected months if MockDate were removed Set explicit defaultMonth in each story
SUGGESTION Memoization OneCalendar/OneCalendar.tsx navigate, getHeaderLabel, and handleSelect are plain const inside render — recreated every render Wrap with useCallback
SUGGESTION useEffect OneCalendar/OneCalendar.tsx Multiple useEffect hooks suppress missing-dep warnings via eslint-disable Fix actual dependency arrays; suppressions can hide stale-closure bugs
SUGGESTION Namespace import OneCalendar/utils.ts import * as locales from "date-fns/locale" — namespace import pulls in entire locale bundle Import only needed locales, or document why namespace import is required
SUGGESTION Today indicator a11y granularities/month/MonthView.tsx Current-month button has no aria-current — AT users cannot identify today's month Add aria-current="date" when isCurrent is true
SUGGESTION Today indicator a11y granularities/year/YearView.tsx Current-year button has no aria-current Add aria-current="date" when isCurrent is true
SUGGESTION Today indicator a11y granularities/quarter/QuarterView.tsx Current-quarter button has no aria-current Add aria-current="date" when isCurrent is true
SUGGESTION Today indicator a11y granularities/halfyear/HalfyearView.tsx Current-half-year button has no aria-current Add aria-current="date" when isCurrent is true
SUGGESTION Decorative elements granularities/quarter/QuarterView.tsx Range-fill <div> elements for isStart/isEnd lack aria-hidden="true" Add aria-hidden="true" to both decorative divs
SUGGESTION Decorative elements granularities/halfyear/HalfyearView.tsx Same — range-fill divs lack aria-hidden="true" Add aria-hidden="true" to both decorative divs
SUGGESTION Live region OneCalendar/OneCalendar.tsx Header label (e.g. "January 2024") updates on navigation but is in a plain <div> with no aria-live Add aria-live="polite" and aria-atomic="true" to the header label container
SUGGESTION Landmark OneCalendar/OneCalendar.tsx Calendar container has no role or aria-label — AT users cannot identify this widget Add role="group" with aria-label from useI18n()
SUGGESTION Heading structure OneEmptyState/OneEmptyState.tsx Title rendered as <p> — screen reader users cannot navigate to it via heading shortcuts Render as <h2> by default, or accept a headingLevel prop
SUGGESTION Non-null assertion OneEmptyState/OneEmptyState.tsx emoji! non-null assertion when emoji is optional Guard explicitly: emoji && <F0AvatarEmoji emoji={emoji} ... />
SUGGESTION Key stability OneEmptyState/OneEmptyState.tsx action.label used as key in actions .map() — labels not guaranteed unique Add an explicit id field to ActionProps or use index as key
SUGGESTION Test coverage OneCalendar/__tests__/OneCalendar.test.tsx Only 1 test case — no coverage of navigation, selection, range mode, min/max constraints, or onSelect callback Add tests for all major interaction paths
SUGGESTION Test typing granularities/day/__tests__/index.test.tsx:10 const i18n = {} typed as empty object — may mask type errors Use a typed fixture matching the TranslationsType shape
SUGGESTION Upsell variant test OneEmptyState/__tests__/ (to create) No tests for UpsellingButton action variant Assert upsell button renders and passes correct props

Copy-pasteable agent fix prompt

You are fixing stability issues in the F0 design system. Fix ALL BLOCKING items below for the OneCalendar and OneEmptyState components in packages/react/src/components/.

1. Move `OneCalendar/OneCalendar.test.tsx` → `OneCalendar/__tests__/OneCalendar.test.tsx`
2. Remove `import React from "react"` from the test file (forbidden default import)
3. Move `OneCalendar/OneCalendar.stories.tsx` → `OneCalendar/__stories__/OneCalendar.stories.tsx`
4. Add `Snapshot` story with `withSnapshot({})` to both story files
5. Add `"stable"` tag to OneCalendar meta tags array
6. Add `OneCalendarInternal.displayName = "OneCalendarInternal"` after the function definition
7. Add `useReducedMotion()` guard to ALL six animated view files (DayView, MonthView, YearView, WeekView, QuarterView, HalfyearView) — set `duration: 0` and skip x-axis motion variants when `useReducedMotion()` returns true
8. Add `type="button"` to `<button>` elements in QuarterView, HalfyearView, and YearView
9. Create `OneCalendar/granularities/range/__tests__/index.test.tsx` with tests for: `toRange`, `toRangeString`, `toString`, `fromString`, `navigate`, `getPrevNext`
10. Fix `OneEmptyState/__stories__/OneEmptyState.stories.tsx:20` — change `StoryObj<typeof OneEmptyState>` to `StoryObj<typeof meta>`
11. Add `Snapshot` story to `OneEmptyState/__stories__/OneEmptyState.stories.tsx`
12. Add `_OneEmptyState.displayName = "OneEmptyState"` in `OneEmptyState/OneEmptyState.tsx`
13. Create `OneEmptyState/__tests__/OneEmptyState.test.tsx` with render/variant/action tests using `zeroRender` from `@/testing/test-utils`
14. Replace `import { Plus } from "lucide-react"` with `import { Plus } from "@/icons/app"` in OneEmptyState stories

Run `pnpm vitest:ci` and `pnpm tsc` from packages/react/ to verify. Fix any failures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions