Skip to content

feat(date-field,calendar): calendar tedi-ready #6#443

Open
mart-sessman wants to merge 3 commits into
feat/374-time-field-picker-tedi-readyfrom
feat/6-date-field-calendar-tedi-ready
Open

feat(date-field,calendar): calendar tedi-ready #6#443
mart-sessman wants to merge 3 commits into
feat/374-time-field-picker-tedi-readyfrom
feat/6-date-field-calendar-tedi-ready

Conversation

@mart-sessman
Copy link
Copy Markdown
Contributor

@mart-sessman mart-sessman commented May 20, 2026

Summary by CodeRabbit

  • New Features

    • Added Calendar component supporting day, month, and year selection modes with single, multiple, and range date selection.
    • Added DateField form component with calendar picker (popover or modal), text input parsing, and native date picker fallback.
    • Added support for customizable disabled/unavailable dates, week number display, and multi-month rendering.
    • Added locale-aware date formatting, parsing, and keyboard navigation for calendar grid views.
    • Enhanced form field integration with ControlValueAccessor implementation.
  • Documentation

    • Updated Storybook with calendar and date field component examples.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • rc
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b2af3abe-ce0c-4100-8905-10a2a8085064

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a comprehensive calendar and date field selection system for the TEDI Design System. It adds a multi-view calendar (day/month/year), a reactive date field form control with locale-aware date utilities, keyboard navigation, and both popover and modal presentation modes, supported by extensive test coverage and Storybook stories.

Changes

Calendar and DateField Feature Implementation

Layer / File(s) Summary
Date utilities and matchers foundation
tedi/utils/date.util.ts, tedi/utils/date.util.spec.ts, tedi/utils/matchers.util.ts, tedi/utils/matchers.util.spec.ts, tedi/utils/index.ts
New locale-aware date helpers (arithmetic via addDays/addMonths/addYears, boundary helpers, month/weekday name generation via Intl, locale-specific date formatting and parsing). Type-safe Matcher union for date filtering with matchDate and matchAny predicates supporting intervals, date ranges, day-of-week, and custom functions.
Calendar day/month/year grid components
tedi/components/content/calendar/calendar-day-grid/*, tedi/components/content/calendar/calendar-month-grid/*, tedi/components/content/calendar/calendar-year-grid/*, tedi/components/content/calendar/calendar-header/*
CalendarDayGridComponent renders a 6×7 month grid with roving tabindex, week numbers, range hover preview, and CSS state classes for selected/disabled/outside days. CalendarMonthGridComponent and CalendarYearGridComponent render 3-column selector grids. CalendarHeaderComponent provides prev/next navigation, month/year dropdowns or grid selection, and view-mode switching with translated ARIA labels.
Main calendar component
tedi/components/content/calendar/calendar.component.ts, tedi/components/content/calendar/calendar.component.html, tedi/components/content/calendar/calendar.component.scss, tedi/components/content/calendar/calendar.component.spec.ts, tedi/components/content/calendar/index.ts
Standalone CalendarComponent implements ControlValueAccessor with reactive signals for view mode (days/months/years), current month, and selection value (single/multiple/range). Computes month/year disabling predicates, derives disabled matchers from availability rules, handles full keyboard navigation (arrows/home/end/page), and orchestrates child grid and header components.
DateField and DateInput form components
tedi/components/form/date-field/date-field.component.ts, tedi/components/form/date-field/date-field.component.html, tedi/components/form/date-field/date-field.component.scss, tedi/components/form/date-field/date-field.component.spec.ts, tedi/components/form/date-field/date-input/*, tedi/components/form/date-field/date-field-modal/*, tedi/components/form/date-field/index.ts
DateFieldComponent wraps the calendar with ControlValueAccessor and FormFieldControl<DateFieldValue> for form integration. Supports text input parsing (custom or locale-aware), native OS date picker fallback, and popover or modal calendar presentation. DateInputComponent renders the low-level input UI with optional chips (multiple mode), clear button, and calendar icon. DateFieldModalComponent presents a calendar inside a modal dialog.
Stories, translations, and form integration
tedi/components/content/calendar/calendar.stories.ts, tedi/components/form/date-field/date-field.stories.ts, tedi/services/translation/translations.ts, tedi/components/form/form-field/form-field.component.html, tedi/components/form/date-picker/date-picker.stories.ts, tedi/components/overlay/modal/modal.stories.ts, tedi/index.ts, .storybook/preview-head.html, tedi/components/form/select/select.component.scss
Comprehensive Storybook stories demonstrating calendar and date field in single/multiple/range modes, with availability, custom formatting, keyboard behavior, and locale variants. Translation keys added for date picker UI (today, week numbers, month/year chooser labels) and date field (chip removal, modal title, confirm/cancel). Form field updated to project date-field elements. Modal stories switched from date-picker to date-field for scrollable variants.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Suggested reviewers

  • ly-tempel-bitweb
  • airikej
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/6-date-field-calendar-tedi-ready

@mart-sessman
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mart-sessman mart-sessman changed the title Feat/6 date field calendar tedi ready feat(date-field,calendar): calendar tedi-ready #6 May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.scss`:
- Around line 30-32: The bottom-border rule using the selector
"&:not(:first-child)" in calendar-day-grid.component.scss never matches the
week-number header cell, so remove the ":not(:first-child)" pseudo-class from
that rule (or replace it with a selector that targets the week-number header
cell explicitly, e.g., the week-number cell class) so the border is applied to
the week-number header; update the rule around the existing selector where
"&:not(:first-child)" appears (the calendar-day-grid cell styling) to use "&" or
the specific week-number selector instead.
- Around line 187-192: Replace the direct element selector styling for
tedi-status-indicator with a BEM-style class: add a class (e.g.
"calendar-day-grid__status" or "tedi-status-indicator") to the
tedi-status-indicator element in the calendar-day-grid template, then update
calendar-day-grid.component.scss to target that class instead of the element
selector; ensure positioning rules (position, top, right, pointer-events) are
moved to the new class and remove the original tedi-status-indicator element
selector.

In
`@tedi/components/content/calendar/calendar-header/calendar-header.component.html`:
- Line 32: Replace the nonstandard property binding ariaHaspopup="listbox" on
the calendar header trigger buttons with an attribute binding so assistive tech
sees the ARIA attribute; use [attr.aria-haspopup]="'listbox'" (matching how
[attr.aria-label] is used) and apply this change to all three trigger buttons in
the template so each trigger sets the actual aria-haspopup HTML attribute.

In
`@tedi/components/content/calendar/calendar-header/calendar-header.component.scss`:
- Around line 18-20: The SCSS rule currently targets the Angular element
selector "tedi-icon" (&.tedi-button tedi-icon) which we should avoid; change the
selector to target a CSS class (e.g., &.tedi-button .tedi-icon) and then add
that class to the icon host in the CalendarHeaderComponent template where the
tedi-icon is used (update the template to <tedi-icon class="tedi-icon"> or
similar). Ensure you keep the same style (font-size: var(--icon-03)) and update
any other usages to use the new class instead of relying on the element
selector.

In
`@tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.html`:
- Around line 10-20: The month buttons in the calendar template only set
aria-disabled but not the native disabled attribute, so disabled months can
still receive focus and clicks; update the button element in
calendar-month-grid.component.html to bind the native disabled attribute using
[disabled]="isDisabled(i)" alongside the existing aria bindings so that
isDisabled(i) prevents native interaction (this affects the button with class
tedi-calendar-month-grid__month and the click handler handleClick(i)).

In `@tedi/components/content/calendar/calendar.stories.ts`:
- Around line 27-57: The module-level mutation of globalThis.Date (symbols
FIXED_TODAY_MS, GLOBAL_MARK, globalScope.Date, MockDate) must be removed;
instead implement a Storybook decorator (e.g., mockTodayDecorator) that, for
each story render, temporarily replaces Date with a MockDate that uses
FIXED_TODAY_MS, calls the story, and then restores the original Date in a
finally block so the global runtime is never permanently mutated; update the
stories export to apply this decorator to calendar stories and remove the
top-level patch/guard code from the module.

In `@tedi/components/form/date-field/date-field.component.html`:
- Around line 50-53: The tedi-calendar usage in date-field.component.html
forwards availableDays but omits unavailableDays, allowing blocked dates to be
selectable; update the tedi-calendar element to forward the unavailableDays
input (e.g., add [unavailableDays]="unavailableDays()") alongside existing
bindings such as [availableDays]="availableDays()",
[disabledMatchers]="disabledMatchers()",
[shouldDisableMonth]="shouldDisableMonth()", and
[shouldDisableYear]="shouldDisableYear()" so the calendar respects upstream
unavailable constraints.

In `@tedi/components/form/date-field/date-field.component.ts`:
- Around line 348-351: The modal open path allows re-entrancy: in the block that
checks useModal() and calls openModal() (and the similar block around the
469-505 region), add a guard using overlayOpen() so you only call openModal()
when overlayOpen() is false; specifically, change the logic so it first checks
if (this.useModal() && !this.overlayOpen()) then call this.openModal() and
return, preventing duplicate opens while a modal is active.

In `@tedi/components/form/date-field/date-input/date-input.component.ts`:
- Around line 103-106: handleInput currently emits inputChange even when the
component is non-interactive; update handleInput(event: Event) to early-return
when the component is disabled or readOnly (use the same guard pattern as the
other handlers, e.g. if (this.disabled || this.readOnly) return;) so it does not
call this.inputChange.emit(target.value) for programmatic or blocked events;
keep the rest of the logic (casting target to HTMLInputElement and emitting)
unchanged.

In `@tedi/components/overlay/modal/modal.stories.ts`:
- Around line 433-437: The confirm() handler currently shows a success toast
(this.toastService.success) but leaves the modal open; update confirm() to also
close the modal after showing the toast by invoking the component's modal-close
API (e.g., call the story/component instance method that closes the modal such
as this.close() or this.modalRef.close(), whichever is defined in this story) so
the action flow completes and the modal is dismissed after confirmation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84a21556-a6b2-4aaf-a936-e01cc6c6c2fc

📥 Commits

Reviewing files that changed from the base of the PR and between 7d19f14 and 57c0dbf.

📒 Files selected for processing (54)
  • .storybook/preview-head.html
  • tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.html
  • tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.scss
  • tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.spec.ts
  • tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.ts
  • tedi/components/content/calendar/calendar-day-grid/index.ts
  • tedi/components/content/calendar/calendar-header/calendar-header.component.html
  • tedi/components/content/calendar/calendar-header/calendar-header.component.scss
  • tedi/components/content/calendar/calendar-header/calendar-header.component.spec.ts
  • tedi/components/content/calendar/calendar-header/calendar-header.component.ts
  • tedi/components/content/calendar/calendar-header/index.ts
  • tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.html
  • tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.scss
  • tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.spec.ts
  • tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.ts
  • tedi/components/content/calendar/calendar-month-grid/index.ts
  • tedi/components/content/calendar/calendar-year-grid/calendar-year-grid.component.html
  • tedi/components/content/calendar/calendar-year-grid/calendar-year-grid.component.scss
  • tedi/components/content/calendar/calendar-year-grid/calendar-year-grid.component.spec.ts
  • tedi/components/content/calendar/calendar-year-grid/calendar-year-grid.component.ts
  • tedi/components/content/calendar/calendar-year-grid/index.ts
  • tedi/components/content/calendar/calendar.component.html
  • tedi/components/content/calendar/calendar.component.scss
  • tedi/components/content/calendar/calendar.component.spec.ts
  • tedi/components/content/calendar/calendar.component.ts
  • tedi/components/content/calendar/calendar.stories.ts
  • tedi/components/content/calendar/index.ts
  • tedi/components/content/calendar/types.ts
  • tedi/components/content/index.ts
  • tedi/components/form/date-field/date-field-modal/date-field-modal.component.spec.ts
  • tedi/components/form/date-field/date-field-modal/date-field-modal.component.ts
  • tedi/components/form/date-field/date-field.component.html
  • tedi/components/form/date-field/date-field.component.scss
  • tedi/components/form/date-field/date-field.component.spec.ts
  • tedi/components/form/date-field/date-field.component.ts
  • tedi/components/form/date-field/date-field.stories.ts
  • tedi/components/form/date-field/date-input/date-input.component.html
  • tedi/components/form/date-field/date-input/date-input.component.scss
  • tedi/components/form/date-field/date-input/date-input.component.spec.ts
  • tedi/components/form/date-field/date-input/date-input.component.ts
  • tedi/components/form/date-field/date-input/index.ts
  • tedi/components/form/date-field/index.ts
  • tedi/components/form/date-picker/date-picker.stories.ts
  • tedi/components/form/form-field/form-field.component.html
  • tedi/components/form/index.ts
  • tedi/components/form/select/select.component.scss
  • tedi/components/overlay/modal/modal.stories.ts
  • tedi/index.ts
  • tedi/services/translation/translations.ts
  • tedi/utils/date.util.spec.ts
  • tedi/utils/date.util.ts
  • tedi/utils/index.ts
  • tedi/utils/matchers.util.spec.ts
  • tedi/utils/matchers.util.ts

Comment on lines +30 to +32
&:not(:first-child) {
border-bottom: var(--tedi-borders-01) solid var(--general-border-primary);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Week-number header border rule is unreachable in current structure.

&:not(:first-child) won’t match the week-number header cell here, so the bottom border is never applied. If the border is intended, remove the pseudo-class guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.scss`
around lines 30 - 32, The bottom-border rule using the selector
"&:not(:first-child)" in calendar-day-grid.component.scss never matches the
week-number header cell, so remove the ":not(:first-child)" pseudo-class from
that rule (or replace it with a selector that targets the week-number header
cell explicitly, e.g., the week-number cell class) so the border is applied to
the week-number header; update the rule around the existing selector where
"&:not(:first-child)" appears (the calendar-day-grid cell styling) to use "&" or
the specific week-number selector instead.

Comment on lines +187 to +192
tedi-status-indicator {
position: absolute;
top: var(--tedi-borders-02);
right: 0;
pointer-events: none;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace direct Angular element selector styling with a BEM class.

Styling tedi-status-indicator directly violates the component SCSS rule; add a class on the element in the template and style that class here instead.

Proposed change
-    tedi-status-indicator {
+    .tedi-calendar-day-grid__status-indicator {
       position: absolute;
       top: var(--tedi-borders-02);
       right: 0;
       pointer-events: none;
     }
-                  <tedi-status-indicator
+                  <tedi-status-indicator
+                    class="tedi-calendar-day-grid__status-indicator"
                     [type]="status.type"
                     size="sm"
                     [hasBorder]="true"
                   />

As per coding guidelines: "Do not style Angular element selectors directly — add CSS classes to component hosts and style those classes instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/content/calendar/calendar-day-grid/calendar-day-grid.component.scss`
around lines 187 - 192, Replace the direct element selector styling for
tedi-status-indicator with a BEM-style class: add a class (e.g.
"calendar-day-grid__status" or "tedi-status-indicator") to the
tedi-status-indicator element in the calendar-day-grid template, then update
calendar-day-grid.component.scss to target that class instead of the element
selector; ensure positioning rules (position, top, right, pointer-events) are
moved to the new class and remove the original tedi-status-indicator element
selector.

<button
tedi-dropdown-trigger
type="button"
ariaHaspopup="listbox"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "calendar-header.component.html" | head -5

Repository: TEDI-Design-System/angular

Length of output: 152


🏁 Script executed:

# Once we find the file, let's examine its content, especially around the mentioned lines
if [ -f "tedi/components/content/calendar/calendar-header/calendar-header.component.html" ]; then
  wc -l "tedi/components/content/calendar/calendar-header/calendar-header.component.html"
fi

Repository: TEDI-Design-System/angular

Length of output: 154


🏁 Script executed:

# Read the file to see the actual attributes
cat -n "tedi/components/content/calendar/calendar-header/calendar-header.component.html"

Repository: TEDI-Design-System/angular

Length of output: 8165


Use the valid ARIA attribute binding syntax for popup triggers.

ariaHaspopup="listbox" should use the [attr.] binding to properly expose the ARIA attribute to assistive technology: [attr.aria-haspopup]="'listbox'". The current syntax creates a property binding instead of setting the HTML attribute, which won't reliably communicate listbox semantics to screen readers. This is inconsistent with other ARIA attributes in the template (e.g., [attr.aria-label]).

Proposed fix
-              ariaHaspopup="listbox"
+              [attr.aria-haspopup]="'listbox'"

Apply the same replacement to all three trigger buttons in this template (lines 32, 85, 138).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ariaHaspopup="listbox"
[attr.aria-haspopup]="'listbox'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/content/calendar/calendar-header/calendar-header.component.html`
at line 32, Replace the nonstandard property binding ariaHaspopup="listbox" on
the calendar header trigger buttons with an attribute binding so assistive tech
sees the ARIA attribute; use [attr.aria-haspopup]="'listbox'" (matching how
[attr.aria-label] is used) and apply this change to all three trigger buttons in
the template so each trigger sets the actual aria-haspopup HTML attribute.

Comment on lines +18 to +20
&.tedi-button tedi-icon {
font-size: var(--icon-03);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Avoid styling Angular element selectors directly.

Please replace the tedi-icon selector with a class on the icon host and style that class instead.

As per coding guidelines, “Do not style Angular element selectors directly — add CSS classes to component hosts and style those classes instead.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/content/calendar/calendar-header/calendar-header.component.scss`
around lines 18 - 20, The SCSS rule currently targets the Angular element
selector "tedi-icon" (&.tedi-button tedi-icon) which we should avoid; change the
selector to target a CSS class (e.g., &.tedi-button .tedi-icon) and then add
that class to the icon host in the CalendarHeaderComponent template where the
tedi-icon is used (update the template to <tedi-icon class="tedi-icon"> or
similar). Ensure you keep the same style (font-size: var(--icon-03)) and update
any other usages to use the new class instead of relying on the element
selector.

Comment on lines +10 to +20
<button
type="button"
class="tedi-calendar-month-grid__month"
[class.tedi-calendar-month-grid__month--selected]="isSelected(i)"
[class.tedi-calendar-month-grid__month--current]="isCurrent(i)"
[class.tedi-calendar-month-grid__month--disabled]="isDisabled(i)"
[attr.aria-label]="monthLongNames()[i]"
[attr.aria-selected]="isSelected(i) ? 'true' : null"
[attr.aria-disabled]="isDisabled(i) ? 'true' : null"
(click)="handleClick(i)"
>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bind native disabled state on month buttons.

aria-disabled alone is not enough for native button behavior; add [disabled]="isDisabled(i)" to prevent accidental keyboard/mouse interaction on disabled months.

Proposed fix
           <button
             type="button"
             class="tedi-calendar-month-grid__month"
             [class.tedi-calendar-month-grid__month--selected]="isSelected(i)"
             [class.tedi-calendar-month-grid__month--current]="isCurrent(i)"
             [class.tedi-calendar-month-grid__month--disabled]="isDisabled(i)"
+            [disabled]="isDisabled(i)"
             [attr.aria-label]="monthLongNames()[i]"
             [attr.aria-selected]="isSelected(i) ? 'true' : null"
             [attr.aria-disabled]="isDisabled(i) ? 'true' : null"
             (click)="handleClick(i)"
           >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="button"
class="tedi-calendar-month-grid__month"
[class.tedi-calendar-month-grid__month--selected]="isSelected(i)"
[class.tedi-calendar-month-grid__month--current]="isCurrent(i)"
[class.tedi-calendar-month-grid__month--disabled]="isDisabled(i)"
[attr.aria-label]="monthLongNames()[i]"
[attr.aria-selected]="isSelected(i) ? 'true' : null"
[attr.aria-disabled]="isDisabled(i) ? 'true' : null"
(click)="handleClick(i)"
>
<button
type="button"
class="tedi-calendar-month-grid__month"
[class.tedi-calendar-month-grid__month--selected]="isSelected(i)"
[class.tedi-calendar-month-grid__month--current]="isCurrent(i)"
[class.tedi-calendar-month-grid__month--disabled]="isDisabled(i)"
[disabled]="isDisabled(i)"
[attr.aria-label]="monthLongNames()[i]"
[attr.aria-selected]="isSelected(i) ? 'true' : null"
[attr.aria-disabled]="isDisabled(i) ? 'true' : null"
(click)="handleClick(i)"
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tedi/components/content/calendar/calendar-month-grid/calendar-month-grid.component.html`
around lines 10 - 20, The month buttons in the calendar template only set
aria-disabled but not the native disabled attribute, so disabled months can
still receive focus and clicks; update the button element in
calendar-month-grid.component.html to bind the native disabled attribute using
[disabled]="isDisabled(i)" alongside the existing aria bindings so that
isDisabled(i) prevents native interaction (this affects the button with class
tedi-calendar-month-grid__month and the click handler handleClick(i)).

Comment on lines +27 to +57
// Lock "today" for Chromatic stability. Patches the global Date constructor
// for this stories module so the calendar's internal `new Date()` checks
// (today indicator, focusable-day fallback) resolve to a fixed reference.
//
// Guard against double-patching: Storybook HMR re-evaluates this module, and
// without the guard each reload would wrap the previous MockDate in another
// subclass — every `new Date()` would then traverse a growing prototype chain,
// pegging CPU once enough reloads have happened (especially in Docs view
// where many calendars instantiate at once).
const FIXED_TODAY_MS = new Date(2026, 4, 16).getTime();
const GLOBAL_MARK = "__tediCalendarMockDate";
const globalScope = globalThis as unknown as Record<string, unknown> & {
Date: typeof Date;
};
if (!globalScope[GLOBAL_MARK]) {
const RealDate = Date;
class MockDate extends RealDate {
constructor(...args: unknown[]) {
if (args.length === 0) {
super(FIXED_TODAY_MS);
return;
}
super(...(args as ConstructorParameters<typeof Date>));
}
static override now(): number {
return FIXED_TODAY_MS;
}
}
globalScope.Date = MockDate as typeof Date;
globalScope[GLOBAL_MARK] = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid mutating globalThis.Date in a stories module.

This patch affects the whole Storybook runtime, so other stories can get a frozen “today” unintentionally and become order-dependent/non-deterministic.

Suggested direction
-// module-level global Date patch
+// Prefer injecting a "now" source into CalendarComponent (e.g., token/service)
+// and override it only in this story via moduleMetadata providers.
+// This keeps story determinism without global side effects.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/content/calendar/calendar.stories.ts` around lines 27 - 57,
The module-level mutation of globalThis.Date (symbols FIXED_TODAY_MS,
GLOBAL_MARK, globalScope.Date, MockDate) must be removed; instead implement a
Storybook decorator (e.g., mockTodayDecorator) that, for each story render,
temporarily replaces Date with a MockDate that uses FIXED_TODAY_MS, calls the
story, and then restores the original Date in a finally block so the global
runtime is never permanently mutated; update the stories export to apply this
decorator to calendar stories and remove the top-level patch/guard code from the
module.

Comment on lines +50 to +53
[disabledMatchers]="disabledMatchers()"
[availableDays]="availableDays()"
[shouldDisableMonth]="shouldDisableMonth()"
[shouldDisableYear]="shouldDisableYear()"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forward unavailableDays into tedi-calendar to preserve disable/availability behavior.

DateField forwards availableDays, but not unavailableDays. This can make blocked dates selectable in popover mode even when unavailable constraints are configured upstream.

Proposed fix
       <tedi-calendar
         `#calendar`
         [bordered]="false"
         [value]="value()"
         [currentMonth]="currentMonth()"
         [mode]="mode()"
         [selectionLevel]="selectionLevel()"
         [localeCode]="localeCode()"
         [showOutsideDays]="showOutsideDays()"
         [numberOfMonths]="numberOfMonthsResolved()"
         [monthYearSelectType]="monthYearSelectType()"
         [required]="required()"
         [disabledMatchers]="disabledMatchers()"
         [availableDays]="availableDays()"
+        [unavailableDays]="unavailableDays()"
         [shouldDisableMonth]="shouldDisableMonth()"
         [shouldDisableYear]="shouldDisableYear()"
         [inputDisabled]="fieldDisabled()"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[disabledMatchers]="disabledMatchers()"
[availableDays]="availableDays()"
[shouldDisableMonth]="shouldDisableMonth()"
[shouldDisableYear]="shouldDisableYear()"
[disabledMatchers]="disabledMatchers()"
[availableDays]="availableDays()"
[unavailableDays]="unavailableDays()"
[shouldDisableMonth]="shouldDisableMonth()"
[shouldDisableYear]="shouldDisableYear()"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/form/date-field/date-field.component.html` around lines 50 -
53, The tedi-calendar usage in date-field.component.html forwards availableDays
but omits unavailableDays, allowing blocked dates to be selectable; update the
tedi-calendar element to forward the unavailableDays input (e.g., add
[unavailableDays]="unavailableDays()") alongside existing bindings such as
[availableDays]="availableDays()", [disabledMatchers]="disabledMatchers()",
[shouldDisableMonth]="shouldDisableMonth()", and
[shouldDisableYear]="shouldDisableYear()" so the calendar respects upstream
unavailable constraints.

Comment on lines +348 to +351
if (this.useModal()) {
this.openModal();
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent duplicate modal opens while one is already active.

In modal mode, repeated icon clicks can trigger openModal() again before the first modal closes, which risks stacked modals and duplicate close handling. Guard this path with overlayOpen().

Proposed fix
   handleIconClick(): void {
     if (this.fieldDisabled()) return;
     if (!this.enableCalendarResolved()) return;
@@
     if (this.useModal()) {
+      if (this.overlayOpen()) return;
       this.openModal();
       return;
     }

Also applies to: 469-505

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/form/date-field/date-field.component.ts` around lines 348 -
351, The modal open path allows re-entrancy: in the block that checks useModal()
and calls openModal() (and the similar block around the 469-505 region), add a
guard using overlayOpen() so you only call openModal() when overlayOpen() is
false; specifically, change the logic so it first checks if (this.useModal() &&
!this.overlayOpen()) then call this.openModal() and return, preventing duplicate
opens while a modal is active.

Comment on lines +103 to +106
handleInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.inputChange.emit(target.value);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard handleInput when disabled/readOnly.

Line 103 currently emits inputChange even if the component is non-interactive (when events are dispatched programmatically). Add the same guard pattern used by the other handlers.

Suggested fix
  handleInput(event: Event): void {
+   if (this.disabled() || this.readOnly()) return;
    const target = event.target as HTMLInputElement;
    this.inputChange.emit(target.value);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handleInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.inputChange.emit(target.value);
}
handleInput(event: Event): void {
if (this.disabled() || this.readOnly()) return;
const target = event.target as HTMLInputElement;
this.inputChange.emit(target.value);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/form/date-field/date-input/date-input.component.ts` around
lines 103 - 106, handleInput currently emits inputChange even when the component
is non-interactive; update handleInput(event: Event) to early-return when the
component is disabled or readOnly (use the same guard pattern as the other
handlers, e.g. if (this.disabled || this.readOnly) return;) so it does not call
this.inputChange.emit(target.value) for programmatic or blocked events; keep the
rest of the logic (casting target to HTMLInputElement and emitting) unchanged.

Comment on lines +433 to +437
confirm() {
const date = this.selectedDate();
const formatted = date ? formatDate(date) : "no date selected";
this.toastService.success("Saved", `Selected date: ${formatted}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Close the modal on confirm to complete the action flow.

confirm() shows a success toast but leaves the modal open, which makes the primary action feel incomplete.

Patch
  confirm() {
    const date = this.selectedDate();
    const formatted = date ? formatDate(date) : "no date selected";
    this.toastService.success("Saved", `Selected date: ${formatted}`);
+   this.ref.close("confirm");
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tedi/components/overlay/modal/modal.stories.ts` around lines 433 - 437, The
confirm() handler currently shows a success toast (this.toastService.success)
but leaves the modal open; update confirm() to also close the modal after
showing the toast by invoking the component's modal-close API (e.g., call the
story/component instance method that closes the modal such as this.close() or
this.modalRef.close(), whichever is defined in this story) so the action flow
completes and the modal is dismissed after confirmation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant