Skip to content

bug(#3497): fix calendar years dropdown when min/max = today#3508

Merged
chrisolsen merged 2 commits into
devfrom
eric/bug3497-years-empty
Mar 23, 2026
Merged

bug(#3497): fix calendar years dropdown when min/max = today#3508
chrisolsen merged 2 commits into
devfrom
eric/bug3497-years-empty

Conversation

@willcodeforcoffee
Copy link
Copy Markdown
Collaborator

@willcodeforcoffee willcodeforcoffee commented Mar 3, 2026

Before (the change)

The Calendar wasn't showing years if the min or max were equal to Today.
There was a problem with data types, parsing and passing strings and JavaScript Dates from Angular components.

After (the change)

This passes the Dates as ISO strings from Angular to web-component. ISO date strings are more easily parsed by the CalendarDate.parse() method.

Make sure that you've checked the boxes below before you submit the PR

  • I have read and followed the setup steps
  • I have created necessary unit tests
  • I have tested the functionality in both React and Angular.

Steps needed to test

  • Create a test page in the PRs or playground with Calendar and DatePicker
  • Set the min or max to today.
  • The year dropdown should be populated with the correct years based on those values.

@willcodeforcoffee willcodeforcoffee self-assigned this Mar 3, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 3, 2026

Deploy Preview for goa-design-2 ready!

Name Link
🔨 Latest commit ba3ad0d
🔍 Latest deploy log https://app.netlify.com/projects/goa-design-2/deploys/69bd912a960c870008271cb4
😎 Deploy Preview https://deploy-preview-3508--goa-design-2.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@willcodeforcoffee willcodeforcoffee marked this pull request as ready for review March 3, 2026 19:08
@willcodeforcoffee willcodeforcoffee marked this pull request as draft March 3, 2026 19:08
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch 3 times, most recently from 61bbe60 to 8d5ac91 Compare March 9, 2026 22:17
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch from 8d5ac91 to 067f7b6 Compare March 9, 2026 22:40
@willcodeforcoffee willcodeforcoffee marked this pull request as ready for review March 9, 2026 23:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses Bug #3497 where the calendar year dropdown can become empty when min/max are set (notably from the Angular wrapper), and adds demo routes in the PR apps to reproduce/verify the behavior.

Changes:

  • Angular calendar wrapper now binds min/max as ISO strings instead of relying on Date.toString().
  • Web component calendar adds warnings when min/max parse as invalid dates.
  • Adds Bug 3497 reproduction pages/routes in both the React and Angular PR apps.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
libs/web-components/src/components/calendar/Calendar.svelte Adds console warnings for invalid min/max inputs while recalculating year options.
libs/angular-components/src/lib/components/calendar/calendar.ts Updates min/max attribute binding to ISO strings; adds initialization logging.
libs/angular-components/src/lib/components/calendar/calendar.spec.ts Updates unit assertions to match ISO string binding for min/max.
apps/prs/react/src/routes/bugs/bug3497.tsx Adds a React reproduction route for the bug scenario.
apps/prs/react/src/main.tsx Registers the new React bug route and reformats some existing routes.
apps/prs/react/src/app/app.tsx Adds a side-menu link to the new React bug route.
apps/prs/angular/src/routes/bugs/3497/bug3497.component.ts Adds an Angular reproduction component for the bug scenario.
apps/prs/angular/src/routes/bugs/3497/bug3497.component.html Adds the Angular reproduction template.
apps/prs/angular/src/app/app.routes.ts Registers the new Angular bug route.
apps/prs/angular/src/app/app.component.html Adds a side-menu link to the new Angular bug route.
Comments suppressed due to low confidence (1)

libs/web-components/src/components/calendar/Calendar.svelte:96

  • The new warnings detect invalid min/max, but the component still uses _min.year/_max.year to compute yearCount. When either date is invalid, yearCount becomes NaN (coerces to 0) and the year dropdown remains empty. Consider falling back to the default min/max (or the last known valid values) when !_min.isValid() / !_max.isValid(), and guard against non-positive/invalid yearCount before building _years.
    _min = min ? new CalendarDate(min) : new CalendarDate().addYears(-5);
    _max = max ? new CalendarDate(max) : new CalendarDate().addYears(+5);
    if (!_min.isValid()) {
      console.warn(`Invalid min date: ${min}.`);
    }
    if (!_max.isValid()) {
      console.warn(`Invalid max date: ${max}.`);
    }

    // Update years list based on new min/max
    const yearStart = _min.year;
    const yearCount = _max.year - yearStart + 1;

    _years = Array.from({ length: yearCount }, (_, i) => `${yearStart + i}`);

You can also share your feedback on Copilot code review. Take the survey.

Comment thread libs/angular-components/src/lib/components/calendar/calendar.ts
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch from 067f7b6 to a798950 Compare March 9, 2026 23:22
@willcodeforcoffee willcodeforcoffee marked this pull request as draft March 9, 2026 23:24
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch 2 times, most recently from bd4d00d to e8ff723 Compare March 10, 2026 16:21
@willcodeforcoffee willcodeforcoffee marked this pull request as ready for review March 10, 2026 16:39
@willcodeforcoffee willcodeforcoffee changed the title bug(#3497): fix calendar years bug(#3497): fix calendar years dropdown when min/max = today Mar 10, 2026
@willcodeforcoffee willcodeforcoffee linked an issue Mar 10, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

libs/web-components/src/components/calendar/Calendar.svelte:116

  • After detecting an invalid min/max you still continue using _min/_max to compute yearStart/yearCount and for boundary comparisons. With an invalid date this can produce an empty _years list and can also lead to runtime errors when calling isBefore/isAfter against an invalid Date. Consider falling back to the default range (e.g., ±5 years from today) or skipping the range update when _min/_max are invalid (and also handle min > max).
    _min = min ? new CalendarDate(min) : new CalendarDate().addYears(-5);
    if (!_min.isValid()) {
      console.error(
        `goa-calendar ${name ?? testid}: Invalid min date provided: ${min}.`,
      );
    }

    _max = max ? new CalendarDate(max) : new CalendarDate().addYears(+5);
    if (!_max.isValid()) {
      console.error(
        `goa-calendar ${name ?? testid}: Invalid max date provided: ${max}.`,
      );
    }

    // Update years list based on new min/max
    const yearStart = _min.year;
    const yearCount = _max.year - yearStart + 1;

    _years = Array.from({ length: yearCount }, (_, i) => `${yearStart + i}`);

    // Adjust calendar if it's outside the new min/max range
    if (_calendarDate) {
      if (_calendarDate.isBefore(_min)) {
        _calendarDate = new CalendarDate(_min);
      } else if (_calendarDate.isAfter(_max)) {
        _calendarDate = new CalendarDate(_max);
      }
    }

    // Re-render with updated values
    renderCalendar({
      type: "date",
      value: _calendarDate || new CalendarDate(),
    });

You can also share your feedback on Copilot code review. Take the survey.

Comment thread libs/web-components/src/components/calendar/Calendar.svelte Outdated
Comment thread libs/angular-components/src/lib/components/calendar/calendar.ts Outdated
Comment thread libs/angular-components/src/experimental/calendar/calendar.ts Outdated
Comment thread apps/prs/angular/src/routes/bugs/3497/bug3497.component.ts

<hr />
<h2>DatePicker — Event-based (min/max = today, Date object)</h2>
<goab-form-item label="Event-based DatePicker">
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see this

Image

@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch 2 times, most recently from e0a4607 to ebb8b81 Compare March 11, 2026 23:19
Comment on lines +51 to +59
minString(): string | undefined {
if (!this.min) return undefined;
return formatDate(this.min);
}

maxString(): string | undefined {
if (!this.max) return undefined;
return formatDate(this.max);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The Date type should show a warning in the console, since React only accepts strings.


if (val instanceof Date) {
return val.toISOString();
return formatDate(val);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The console warning is needed here as well for the min/max dates that should be strings.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 27 changed files in this pull request and generated 10 comments.


You can also share your feedback on Copilot code review. Take the survey.

Comment thread libs/angular-components/src/lib/components/calendar/calendar.ts Outdated
Comment thread libs/react-components/vite.config.ts Outdated
Comment on lines +45 to +49
resolve: {
alias: {
"@abgov/ui-components-common": path.resolve(
__dirname,
"../../node_modules/@abgov/ui-components-common",
Comment thread apps/prs/angular/src/routes/bugs/3497/bug3497.component.ts Outdated
Comment on lines +86 to +94
`goa-calendar ${name ?? testid}: Invalid min date provided: ${min}.`,
);
}

_max = max ? new CalendarDate(max) : new CalendarDate().addYears(+5);
if (!_max.isValid()) {
console.error(
`goa-calendar ${name ?? testid}: Invalid max date provided: ${max}.`,
);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a good catch, but I don't want to set a value in case of an error. In a case like this I would rather the control act broken than to silently allow invalid input.

Comment thread libs/angular-components/src/lib/components/date-picker/date-picker.ts Outdated
Comment thread libs/react-components/vite.config.ts Outdated
Comment thread vitest.config.mjs
Comment thread vitest.config.mjs
include: [
"libs/web-components/src/**/*.{test,spec}.ts",
],
include: ["libs/web-components/src/**/*.{test,spec}.ts"],
if (!_max.isValid()) {
console.error(
`goa-calendar ${name ?? testid}: Invalid max date provided: ${max}.`,
);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

As with the comment above, I don't want to set a value in the case of an invalid input. In a case like this I would rather the control act broken than to silently allow invalid input.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that the suggested code would be a good thing to have, I don't like the fact that this logic has to be done every single time. Maybe the following code would be a little cleaner..

  constructor(value?: CalendarDateInput, fallback?: CalendarDate) {
    if (value || value === 0) {
      this._dateNums = CalendarDate.parse(value);
      if (!this.isValid()) {
        if (fallback) {
          this._dateNums = [fallback.year, fallback.month, fallback.day];
        } else {
          this._dateNums = [0, 0, 0];
        }
        console.error(
          `The CalendarDate value of ${value}, is invalid. Falling back to ${this.date}`,
        );
      }
    } else {
      this._dateNums = CalendarDate.parse(new Date());
    }
  }

This would then allow for

_min = new CalendarDate(min, new CalendarDate().addYears(-5));

It is a little weird in using a CalendarDate as a fallback for the CalendarDate constructor, but it does clean things up a bit.

What do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unresolving this comment for discussion.

Copy link
Copy Markdown
Collaborator Author

@willcodeforcoffee willcodeforcoffee Mar 18, 2026

Choose a reason for hiding this comment

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

@chrisolsen A default might be okay for value, but I don't think so for min/max.. The main problem I see is that the programmer is specifying a min/max range for value (even though it is somehow invalid), and by falling back to a default we could end up letting bad data slip through because we used a fallback without a warning and they couldn't handle it client-side..

I would also suggest we don't provide fallbacks in the constructor, but rather to the parse method. If the constructor gets invalid input its usually better to raise an exception. Parsing is a situation where the possibility of invalid inputs might be more common. Using a non-ctor method would also allow us to return a non-CalendarDate object like null or undefined.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Parse is a private method so the logic could be moved into it, but the constructor still has to accept it.

The parse method returns an array of numbers which is something not that useful to be used externally, so there is no way of using it.

Comment thread libs/web-components/src/components/calendar/Calendar.svelte Outdated
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch 3 times, most recently from cd46d8e to 27ca9b5 Compare March 17, 2026 15:12
Comment thread apps/prs/angular/src/app/app.component.html
Copy link
Copy Markdown
Collaborator

@vanessatran-ddi vanessatran-ddi left a comment

Choose a reason for hiding this comment

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

I have only 1 comment, I tested and everything works perfectly.

private minWarningLogged = false;
private maxWarningLogged = false;

valueString(): string | undefined {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This file uses 3 separate methods + 3 boolean flags, while date-picker.ts uses a single formatValue(param,value). Should we align this approach? The formatValue pattern in date-picker.ts is cleaner and would reduce duplication here.

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@vanessatran-ddi I thought about this but couldn't decide. I'm glad you suggested this.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this could be cleaned up even more with something like

// This class would exist within the common lib (name of the class and methods are not set in stone)
class DoOnce {
  private done: Set<string>;

  private constructor() {
    this.done = new Set();
  }

  do(id: string, fn): string {
    if (!this.done.has(id)) {
      this.done.add(id);
      fn();
    }
  }
}

// declared within the angular component
const once = new DoOnce();

// angular compoent functions
minString(val: Date | string | null): string {
  if (val instanceof Date) {
    once.do("min", () => console.warn("ya, that's deprecated"));
  }
  return new CalendarDate(val).toString();
}

maxString(val: Date | string | null): string {
  if (val instanceof Date) {
    once.do("max", () => console.warn("ya, that's deprecated"));
  }
  return new CalendarDate(val).toString();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@chrisolsen it seemed like something that might be useful in the polyfills, too, but honestly it doesn't feel like a huge win. After Prettier doing what Prettier does it onto multiple lines and looks like this:

  valueString(): string | undefined {
    if (this.value instanceof Date) {
      this.once.do("value", () => {
        console.warn(
          `GoabxCalendar: Using Date for 'value' is deprecated. Use a string in YYYY-MM-DD format instead.`,
        );
      });
    }
    return this.formatValue("value", this.value);
  }

I tried to cut out the if block to remove some indentation and it looks like this:

  valueString(): string | undefined {
    this.once.when(this.value instanceof Date).do("value", () => {
      console.warn(
        `GoabxCalendar: Using Date for 'value' is deprecated. Use a string in YYYY-MM-DD format instead.`,
      );
    });
    return this.formatValue("value", this.value);
  }

I'm not sure its a huge win, but if you like it I can check it in.

The Once class looks like this:

export class Once {
  private done: Set<string>;

  constructor() {
    this.done = new Set();
  }

  when(condition: boolean): Once {
    if (condition) {
      return this;
    } else {
      return new DoNothing();
    }
  }

  do(id: string, fn: () => void): Once {
    if (!this.done.has(id)) {
      this.done.add(id);
      fn();
    }
    return this;
  }
}

class DoNothing extends Once {
  override do(id: string, fn: () => void): Once {
    return this;
  }
}

Copy link
Copy Markdown
Collaborator

@chrisolsen chrisolsen Mar 19, 2026

Choose a reason for hiding this comment

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

I like your version better.

The thing is that right now this pattern is being done 4 times, and each time you have to create a Set and add the logic. I can also see the Once being useful in other scenarios as well.

@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch from 27ca9b5 to a243a64 Compare March 18, 2026 18:22
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch 2 times, most recently from 789a0a1 to ba3ad0d Compare March 20, 2026 18:25
…pass Dates to the Svelte Calendar component

- make CalendarDate global and use it for date formatting
- fix date handling in React components
- add Once library to execute code only once
- normalize string values at the formatValue level for value, min, max
@willcodeforcoffee willcodeforcoffee force-pushed the eric/bug3497-years-empty branch from ba3ad0d to a9c5482 Compare March 20, 2026 21:46
(From Copilot) The Vitest alias for "@abgov/ui-components-common" points at "./dist/libs/common/index.js". This will fail (or test stale code) when running tests without a prior build of the common library. Prefer resolving to the source entry via tsconfig paths ("libs/common/src/index.ts") or ensure the test command builds common before Vitest runs.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@chrisolsen chrisolsen merged commit afe172f into dev Mar 23, 2026
4 of 5 checks passed
@chrisolsen chrisolsen deleted the eric/bug3497-years-empty branch March 23, 2026 16:50
@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.0.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.0.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 7.0.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 5.0.0-next.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen chrisolsen added the released Released into production. label Apr 1, 2026
@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 7.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chrisolsen
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 5.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

released on @next released Released into production.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Year option empty when min or max is set to today

4 participants