Two entangled defects in experimental/date-range-v2/datepicker/picker.component.ts. Neither can be fixed alone — fixing the first alone makes the picker unopenable by mouse — so they are tracked together.
What to build
Defect 1 — calendarpopup never resolves, so outside-click-to-close is dead code
calendarpopup is declared @ViewChild("calendarpopup", { static: true }) but the element it targets is inside an *ngIf="showCalendar" block. A static query resolves once, before the first change detection run, while showCalendar is still false — so calendarpopup is undefined and, being static, is never updated.
handleGlobalClick guards on this.calendarpopup, so its entire body is unreachable:
- Clicking outside the calendar does not close it.
enablePageTabIndex() never runs on that path. Since disablePageTabIndex() sets tabindex="-1" and aria-hidden="true" on every focusable element on the page when the calendar opens, that state is never restored. After opening the calendar and clicking away, the rest of the page is left unfocusable and hidden from screen readers — an accessibility trap.
Defect 2 — strict target comparison treats the icon's own child as an outside click
handleGlobalClick compares:
this.calendarButton.nativeElement !== event.target
The calendar button contains a screen-reader child span:
<span class="fa fa-calendar" #calendarButton (click)="onInputClick()">
<span class="sr-only">Calendar</span>
</span>
A real mouse click at the icon's centre lands on the inner .sr-only span, not on #calendarButton, so strict inequality treats the button's own child as an outside click. This is latent today (masked by Defect 1), but as soon as calendarpopup resolves, the opening click immediately closes the calendar again in the same event — verified with a MutationObserver showing the popup added and removed ~1.8ms apart. The picker becomes impossible to open by mouse (keyboard Enter still works, since it dispatches no click).
The comparison should test containment, e.g. !this.calendarButton.nativeElement.contains(event.target).
Background
Found while manually testing #660, which changed the @ViewChild to non-static. That change was reverted from #660 because on its own it introduced Defect 2's user-visible symptom. Two specs in picker.spec.ts are it.skipped with pointers to this issue:
should close the calendar when clicking outside of it — cannot pass while the ViewChild is static.
should not close the calendar when clicking the calendar button — passes only vacuously (the guard early-returns because calendarpopup is undefined), so it asserts nothing.
Note that a unit spec calling handleGlobalClick directly with a synthetic { target } object passes under both the broken and fixed comparison, and jsdom does not reproduce the hit-testing behaviour that makes a real click land on the child span. Verification must use a real browser click via Playwright.
Acceptance criteria
Blocked by
Note this component also pulls in moment (a CommonJS dependency that produces a build warning) and Angular forms plumbing, which the tabs harness in #665 does not exercise — expect a small amount of additional harness setup beyond adding the route.
Two entangled defects in
experimental/date-range-v2/datepicker/picker.component.ts. Neither can be fixed alone — fixing the first alone makes the picker unopenable by mouse — so they are tracked together.What to build
Defect 1 —
calendarpopupnever resolves, so outside-click-to-close is dead codecalendarpopupis declared@ViewChild("calendarpopup", { static: true })but the element it targets is inside an*ngIf="showCalendar"block. A static query resolves once, before the first change detection run, whileshowCalendaris stillfalse— socalendarpopupisundefinedand, being static, is never updated.handleGlobalClickguards onthis.calendarpopup, so its entire body is unreachable:enablePageTabIndex()never runs on that path. SincedisablePageTabIndex()setstabindex="-1"andaria-hidden="true"on every focusable element on the page when the calendar opens, that state is never restored. After opening the calendar and clicking away, the rest of the page is left unfocusable and hidden from screen readers — an accessibility trap.Defect 2 — strict target comparison treats the icon's own child as an outside click
handleGlobalClickcompares:The calendar button contains a screen-reader child span:
A real mouse click at the icon's centre lands on the inner
.sr-onlyspan, not on#calendarButton, so strict inequality treats the button's own child as an outside click. This is latent today (masked by Defect 1), but as soon ascalendarpopupresolves, the opening click immediately closes the calendar again in the same event — verified with a MutationObserver showing the popup added and removed ~1.8ms apart. The picker becomes impossible to open by mouse (keyboard Enter still works, since it dispatches no click).The comparison should test containment, e.g.
!this.calendarButton.nativeElement.contains(event.target).Background
Found while manually testing #660, which changed the
@ViewChildto non-static. That change was reverted from #660 because on its own it introduced Defect 2's user-visible symptom. Two specs inpicker.spec.tsareit.skipped with pointers to this issue:should close the calendar when clicking outside of it— cannot pass while the ViewChild is static.should not close the calendar when clicking the calendar button— passes only vacuously (the guard early-returns becausecalendarpopupisundefined), so it asserts nothing.Note that a unit spec calling
handleGlobalClickdirectly with a synthetic{ target }object passes under both the broken and fixed comparison, and jsdom does not reproduce the hit-testing behaviour that makes a real click land on the child span. Verification must use a real browser click via Playwright.Acceptance criteria
calendarpopupresolves when the calendar is open (non-static query, or another approach that makeshandleGlobalClickreachable)handleGlobalClickuses containment rather than strict identity when comparingevent.targetagainst the calendar buttonsam-datepicker-v2with a real mouse click on the calendar icon and asserts the calendar is open and stays opentabindex/aria-hiddenon previously-focusable page elements are restored after the calendar closes via outside clickit.skipped specs inpicker.spec.tsare un-skipped or replaced by equivalent coverage, with no vacuously-passing assertions left behindBlocked by
test-appPlaywright rendering harness (librarypathsmapping, routed gallery shell,BrowserAnimationsModule).test-appas committed cannot render any library component, so this issue needs that foundation before a/datepickerroute can be added. Once Fix inactive tab content rendering on top of active tab in sam-tabs-next #665 lands, the work here is "add a/datepickergallery route +test-app/e2e/datepicker.spec.ts".Note this component also pulls in
moment(a CommonJS dependency that produces a build warning) and Angular forms plumbing, which the tabs harness in #665 does not exercise — expect a small amount of additional harness setup beyond adding the route.