Skip to content

feat(float-ui): replaced float-ui usage in relevant components #390#401

Open
mart-sessman wants to merge 5 commits into
feat/374-time-field-picker-tedi-readyfrom
feat/380-replace-ngx-float-ui
Open

feat(float-ui): replaced float-ui usage in relevant components #390#401
mart-sessman wants to merge 5 commits into
feat/374-time-field-picker-tedi-readyfrom
feat/380-replace-ngx-float-ui

Conversation

@mart-sessman
Copy link
Copy Markdown
Contributor

@mart-sessman mart-sessman commented Apr 8, 2026

BREAKING CHANGES: ngx-float-ui dependency removed, appendTo inputs removed from related components

Summary by CodeRabbit

Release Notes

  • Chores

    • Removed ngx-float-ui dependency from package and documentation.
  • Refactor

    • Migrated dropdown, popover, and tooltip overlay components from external floating UI library to Angular CDK for improved maintainability and reduced external dependencies.
    • Removed appendTo input from overlay components; overlays now append to CDK overlay container by default.
    • Enhanced positioning and overflow prevention using CDK's connected overlay positioning system.
  • Documentation

    • Updated contributing guides to include code review step using /simplify command for scanning code quality and efficiency.
    • Updated README to reflect dependency changes in Angular version management.

BREAKING CHANGES: ngx-float-ui dependency removed, appendTo inputs removed from related components
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 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: 4bb0e9de-fc98-439e-862c-981cc27a257b

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 migrates overlay components (dropdown, popover, tooltip) from the ngx-float-ui library to Angular CDK's overlay system. It removes the ngx-float-ui dependency, adds a new positioning utility module, updates component public APIs, and revises documentation to include a code review step with the /simplify command.

Changes

Cohort / File(s) Summary
Documentation & Contributing Guides
.claude/skills/contributing/references/new-component.md, .claude/skills/contributing/references/refactoring.md, README.md, skills/tedi-angular/SKILL.md
Added "Code Review" step with /simplify instruction in workflow guides; updated README to remove ngx-float-ui from peer dependency modification guidance; removed ngx-float-ui from documented peer dependencies.
Dependency Management
package.json
Removed ngx-float-ui from both peerDependencies and devDependencies.
Configuration & CI
.stylelintrc.json, .github/workflows/angular-test-and-lint.yml
Updated BEM selector regex pattern for modifiers; removed ngx-float-ui@${{ matrix.angular-version }} from npm install in test workflow.
Component API Documentation
skills/tedi-angular/references/components.md
Removed appendTo input documentation and example usages from filter, dropdown, popover, and tooltip component references.
Overlay Positioning Utility
tedi/components/overlay/overlay-position.util.ts, tedi/components/overlay/index.ts
Added new CDK overlay positioning utility with OverlayPosition type, toConnectedPositions(), arrow offset calculation, and HorizontalPushHandler for viewport push behavior.
Dropdown Component Migration
tedi/components/overlay/dropdown/dropdown.component.ts, tedi/components/overlay/dropdown/dropdown.component.html, tedi/components/overlay/dropdown/dropdown.component.scss, tedi/components/overlay/dropdown/dropdown.component.spec.ts, tedi/components/overlay/dropdown/dropdown-trigger/dropdown-trigger.directive.ts, tedi/components/overlay/dropdown/dropdown.stories.ts, tedi/components/overlay/dropdown/dropdown.tokens.ts
Replaced ngx-float-ui with CDK overlay; updated positioning logic, state management to use isOpen(), added CdkOverlayOrigin integration; refactored tests to query overlay container; removed appendTo input.
Popover Component Migration
tedi/components/overlay/popover/popover.component.ts, tedi/components/overlay/popover/popover.component.html, tedi/components/overlay/popover/popover.component.scss, tedi/components/overlay/popover/popover.component.spec.ts, tedi/components/overlay/popover/popover-trigger/popover-trigger.directive.ts, tedi/components/overlay/popover/popover-content/popover-content.component.spec.ts
Migrated from ngx-float-ui to CDK overlay; introduced currentPlacement, isOpen state; added arrow positioning support; integrated CdkOverlayOrigin; removed appendTo input.
Tooltip Component Migration
tedi/components/overlay/tooltip/tooltip.component.ts, tedi/components/overlay/tooltip/tooltip.component.html, tedi/components/overlay/tooltip/tooltip.component.scss, tedi/components/overlay/tooltip/tooltip.component.spec.ts, tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts, tedi/components/overlay/tooltip/tooltip.stories.ts
Migrated from ngx-float-ui to CDK overlay; updated positioning and arrow rendering; integrated CdkOverlayOrigin in trigger; removed appendTo input and stories configuration.
Filter & Date-Picker Components
tedi/components/form/filter/filter.component.ts, tedi/components/form/filter/filter.component.html, tedi/components/form/filter/filter.component.spec.ts, tedi/components/form/filter/filter.stories.ts, tedi/components/form/date-picker/date-picker.component.ts, tedi/components/form/date-picker/date-picker.component.html, tedi/components/form/date-picker/date-picker.component.spec.ts
Updated popover/dropdown state checks from floatUiComponent().state to isOpen(); removed appendTo input from filter; updated control methods to use showPopover()/hidePopover(); refactored tests to use overlay container.
Header Components
tedi/components/layout/header/header-language/header-language.component.ts, tedi/components/layout/header/header-language/header-language.component.html, tedi/components/layout/header/header-profile/header-profile.component.html, tedi/components/layout/header/header-role/header-role.component.html
Removed appendTo bindings from popover/dropdown templates; updated popover hide calls from floatUiComponent().hide() to hidePopover().

Sequence Diagram(s)

sequenceDiagram
    participant Component as Dropdown Component
    participant Origin as CdkOverlayOrigin<br/>(Trigger)
    participant CDK as CDK Overlay<br/>Service
    participant Panel as Overlay Panel<br/>(Content)
    participant Util as overlay-position<br/>Utility

    Component->>Origin: attach CdkOverlayOrigin
    Component->>Util: toConnectedPositions(placement)
    Util-->>Component: ConnectedPosition[]
    
    Component->>Component: set isOpen() = true
    Component->>CDK: [cdkConnectedOverlay]="isOpen()"
    CDK->>CDK: compute overlay position
    CDK->>Util: query positioning logic
    Util-->>CDK: calculated offset
    CDK->>Panel: render with position
    
    Panel-->>Component: (positionChange) event
    Component->>Util: update currentPlacement
    Util-->>Component: resolved placement
    
    Component->>Component: set isOpen() = false
    CDK->>Panel: hide overlay
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ly-tempel-bitweb
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: replacing ngx-float-ui with Angular CDK across multiple components throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/380-replace-ngx-float-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mart-sessman
Copy link
Copy Markdown
Contributor Author

@coderabbitui review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

@mart-sessman: Sure! I'll start the review of PR #401 now.


🧠 Learnings used
Learnt from: CR
Repo: TEDI-Design-System/angular PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-06T14:26:27.098Z
Learning: Applies to tedi/**/*.component.ts : Use Angular signals API (`input()`, `model()`, `signal()`, `computed()`) instead of `Input()`, `Output()`, `ViewChild()` decorators
✅ 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.

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tedi/components/overlay/popover/popover.component.ts (2)

147-179: ⚠️ Potential issue | 🟠 Major

Store lockScroll state at open time to avoid stale overflow management on close.

When the popover opens, showPopover() reads lockScroll() and conditionally locks scroll. When it closes, hidePopover() reads lockScroll() again and conditionally unlocks—but if the signal changed to false while open, the unlock is skipped, leaving document.body overflow permanently hidden. Instead, store the state at open time and restore it based on that state, not the current signal value. The same pattern applies to hideOnScroll and dismissible: if they change from false to true while the popover is open, the listeners won't be attached because onOverlayAttach() only runs once. Use effect() to watch these inputs and re-run setup/cleanup as needed, or store initial state and restore it consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/popover/popover.component.ts` around lines 147 - 179,
Store the boolean values used at open time instead of re-reading reactive
signals on close and to handle dynamic inputs: in showPopover() capture and save
this._lockedScrollAtOpen = this.lockScroll(), this._hideOnScrollAtOpen =
this.hideOnScroll(), and this._dismissibleAtOpen = this.dismissible(); use
_lockedScrollAtOpen in hidePopover() to decide whether to restore document.body
overflow; in onOverlayAttach() use the captured _hideOnScrollAtOpen and
_dismissibleAtOpen to decide whether to call setupScrollListener() and
setupDismissListeners(), and add an effect() that watches lockScroll(),
hideOnScroll(), and dismissible() to attach or detach listeners via
setupScrollListener()/teardownScrollListener() and
setupDismissListeners()/teardownDismissListeners() so listeners are reconciled
while the popover is open (refer to showPopover(), hidePopover(),
onOverlayAttach(), setupScrollListener(), setupDismissListeners()).

158-180: ⚠️ Potential issue | 🟠 Major

Filter overlay descendants from next-focus resolution to prevent tabbing into detached overlay.

When focusElementAfterTrigger() is called (lines 325–336), it scans the entire body for focusable elements while the popover overlay is still in the DOM. If the trigger element is at the end of the page's focusable list, the next element in the list could be a child of the popover. After hidePopover() removes the overlay, the queued focus() call targets a now-detached element, breaking keyboard navigation.

Solution: Filter out overlay descendants from getFocusableElements(this.document.body) before determining the next focus target, or resolve the next target after hidePopover() completes. The same issue applies to focusElementBeforeTrigger() (lines 338–349).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/popover/popover.component.ts` around lines 158 - 180,
The focus restoration routine can pick a focusable element inside the
still-attached overlay and later call focus() on a detached node; update
focusElementAfterTrigger() and focusElementBeforeTrigger() to exclude overlay
descendants when computing getFocusableElements(this.document.body) (filter out
elements contained in this.connectedOverlay()?.overlayRef?.overlayElement or the
".tedi-popover__container") before picking the next/previous target, or
alternatively compute the target after hidePopover() completes; ensure
hidePopover(), focusElementAfterTrigger(), and focusElementBeforeTrigger() are
updated to use this filtered list so focus() never targets detached overlay
elements.
tedi/components/overlay/dropdown/dropdown.component.ts (1)

2-10: ⚠️ Potential issue | 🟠 Major

Wrap the self-provider in forwardRef().

Angular's DI and the project standards require forwardRef() when a component references itself in its own providers array. The decorator metadata is evaluated before the class definition is fully available, so the reference must be deferred. The codebase consistently applies this pattern across all similar providers (form controls with NG_VALUE_ACCESSOR, and tokens like DROPDOWN_CONTENT_API).

Suggested fix
 import {
   Component,
   input,
+  forwardRef,
   ViewEncapsulation,
   ChangeDetectionStrategy,
   contentChild,
   signal,
   computed,
   model,
 } from "@angular/core";
@@
   providers: [
     {
       provide: DROPDOWN_API,
-      useExisting: DropdownComponent,
+      useExisting: forwardRef(() => DropdownComponent),
     },
   ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/dropdown/dropdown.component.ts` around lines 2 - 10,
The component's self-provider in dropdown.component.ts must be wrapped with
Angular's forwardRef to defer the class reference; update the providers entry
that references the component itself (e.g., where DropdownComponent is provided
for NG_VALUE_ACCESSOR or DROPDOWN_CONTENT_API) to use forwardRef(() =>
DropdownComponent), and add import { forwardRef } from '@angular/core' if
missing so the decorator metadata no longer references the class before it's
defined.
♻️ Duplicate comments (1)
.claude/skills/contributing/references/refactoring.md (1)

43-46: ⚠️ Potential issue | 🟡 Minor

Document the /simplify command and clarify "valid findings".

The new code review step introduces the /simplify command without explanation. Users need to know:

  • What the /simplify command is and what it does
  • Where/how to execute it (CLI, IDE plugin, web interface, etc.)
  • What kind of output to expect
  • How to determine which findings are "valid" (criteria, judgment process)

Consider adding a brief explanation or a reference link to documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/contributing/references/refactoring.md around lines 43 - 46,
Update "Step 5: Code Review" to document the /simplify command and define "valid
findings": add a short description of what /simplify does and where it runs
(CLI/IDE/web), show the exact invocation or UI steps to execute it, describe
expected output format (types of findings, severity levels, examples), and
provide clear criteria for marking findings as "valid" (e.g., correctness,
performance, readability, duplication) plus a link to further docs or help;
reference the Step 5 header and the literal "/simplify" command so reviewers can
find and expand the current text.
🧹 Nitpick comments (3)
tedi/components/overlay/overlay-position.util.ts (1)

184-195: Narrow the placement type used by the arrow helpers.

calculateArrowOffset() only handles the four resolved sides, but both it and getPlacementFromPositionChange() are typed as string. That lets raw values like "auto" or "top-start" compile here, and those inputs fall into the wrong branch in the arrow math.

♻️ Suggested typing cleanup
+type ResolvedOverlaySide = "top" | "bottom" | "left" | "right";
+
 export function getPlacementFromPositionChange(
   change: ConnectedOverlayPositionChange,
-): string {
+): ResolvedOverlaySide {
 
 export function calculateArrowOffset(
-  placement: string,
+  placement: ResolvedOverlaySide,
   triggerEl: HTMLElement,
   overlayEl: HTMLElement,

Also applies to: 272-308

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/overlay-position.util.ts` around lines 184 - 195,
Change the loose string typing to a strict placement union so only the four
resolved sides are allowed: update getPlacementFromPositionChange to return a
union type ('top' | 'bottom' | 'left' | 'right') instead of string, and narrow
any callers/related helpers (notably calculateArrowOffset) to accept that same
placement union; ensure getPlacementFromPositionChange maps
ConnectedOverlayPositionChange.connectionPair values into that union only and
update the arrow helper signatures (and any other usages in the 272-308 range)
to use the new placement type so invalid values like "auto" or "top-start" are
rejected at compile time.
tedi/components/layout/header/header-language/header-language.component.ts (1)

3-12: Use the signal query API for the popover reference.

Since this handler is already being touched, migrating the popover query off @ViewChild keeps the file consistent with the repo’s signal-based component pattern and removes the last decorator-style query from this component.

♻️ Suggested refactor
 import {
   ChangeDetectionStrategy,
   Component,
   computed,
-  forwardRef,
   inject,
   input,
   output,
-  ViewChild,
   ViewEncapsulation,
+  viewChild,
 } from "@angular/core";
@@
-  `@ViewChild`(forwardRef(() => PopoverComponent)) popover?: PopoverComponent;
+  popover = viewChild(PopoverComponent);
@@
-    this.popover?.hidePopover();
+    this.popover()?.hidePopover();

As per coding guidelines, "Use Angular signals API (input(), model(), signal(), computed()) instead of @Input(), @Output(), @ViewChild() decorators".

Also applies to: 60-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/layout/header/header-language/header-language.component.ts`
around lines 3 - 12, Replace the `@ViewChild` decorator-based popover query with a
signal-based query: remove the ViewChild import and the `@ViewChild`(...) popover
property, add a signal like popoverRef = signal<HTMLElement | null>(null) (or
the appropriate component type), and ensure the template uses a template
reference variable (e.g. `#popover`) and the component sets popoverRef (e.g. in
ngAfterViewInit or via a small template binding/handler) so all usages that
referenced the old popover property now read from popoverRef() (or computed()
wrappers) — update any code that accessed the old popover symbol to use the new
popoverRef signal.
tedi/components/overlay/popover/popover.component.spec.ts (1)

205-210: Don't bypass the overlay's real attach lifecycle in these specs.

CdkConnectedOverlay already exposes an attach output. Calling component.onOverlayAttach() directly here means these tests will still pass if the template binding is removed or stops firing, so they no longer guard the real overlay lifecycle wiring. Prefer opening the popover and asserting the event-driven path, or keep one focused unit test for onOverlayAttach() itself. (next.material.angular.dev)

Also applies to: 221-225, 235-237, 319-321, 394-396, 413-415

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/popover/popover.component.spec.ts` around lines 205 -
210, Tests call component.onOverlayAttach() directly which bypasses the
template-driven CdkConnectedOverlay.attach event; instead locate the
CdkConnectedOverlay directive instance in the test (query the overlay directive
from the fixture), and drive the lifecycle by emitting its attach output (e.g.,
overlayDirective.attach.emit()) or by opening the popover with
component.showPopover() and asserting the attach emitter is fired; replace
direct onOverlayAttach() calls with this event-driven emit in the affected specs
(references: component.showPopover(), onOverlayAttach(),
CdkConnectedOverlay.attach); keep one focused unit test that directly calls
onOverlayAttach() only if you want to verify its internal behavior separately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/skills/contributing/references/new-component.md:
- Around line 57-60: Update "Step 6: Code Review" to document the /simplify
command and define "valid findings": add a short description of what /simplify
does (e.g., automated code-review assistant that suggests reuse, quality, and
efficiency improvements), where to run it (CLI command or IDE/web integration)
and what output looks like (summary of issues, severity levels, suggested fixes,
and example output). Also add criteria for "valid findings" (e.g., reproducible
issues, non-whitespace changes, security/bug/performance regressions, and
clearly actionable suggestions) and a short guidance on how to triage
suggestions (accept, request changes, or ignore with reason); include a
reference link to the tool/docs if available and mention the command name
"/simplify" and the Step heading ("Step 6: Code Review") so reviewers can find
and run it.

In @.stylelintrc.json:
- Around line 9-11: The lint message for the class selector rule incorrectly
states selectors must start with 'tedi-' even though the regex pattern
"^((tedi|cdk)-...|ng-... )$" allows 'tedi-', 'cdk-' and 'ng-' prefixes; update
the message string (the second item paired with that regex) to list all allowed
prefixes and keep the BEM guidance (e.g., "Class selector must start with
'tedi-', 'cdk-' or 'ng-' prefix and follow BEM naming (e.g., .tedi-button,
.cdk-modal, .ng-component, .tedi-button__icon, .tedi-button--primary). Selector:
\"%s\"").

In
`@tedi/components/overlay/dropdown/dropdown-trigger/dropdown-trigger.directive.ts`:
- Around line 66-77: openAndFocusFirst and openAndFocusLast call showDropdown
then schedule an extra setTimeout-based focus (focusFirstItem/focusLastItem)
which conflicts with DropdownComponent.showDropdown's own queued
focusActiveItem; remove the competing timer and instead add an option to
DropdownComponent.showDropdown (e.g., showDropdown({ initialFocus?:
'auto'|'first'|'last' })) or a boolean flag to suppress its default post-open
focusActiveItem so the caller can request first/last focus without a second
deferred focus; update openAndFocusFirst/openAndFocusLast to call showDropdown
with the new option (and remove the setTimeout calls) or, alternatively, expose
a parameter to skip the internal focus and let these methods perform the single
focus pass.

In `@tedi/components/overlay/dropdown/dropdown.component.html`:
- Around line 3-7: The outside-click handler currently always refocuses the
trigger via onOutsideClick(), breaking pointer interaction; change the template
to pass the event to the handler (overlayOutsideClick)="onOutsideClick($event)"
and update dropdown.component.ts onOutsideClick(event) to only refocus the
trigger when the dismissal is from keyboard (e.g., event is a KeyboardEvent or
event.key === 'Escape') and for pointer-driven events (MouseEvent) simply close
the menu without moving focus; keep existing close logic and ensure any
keyboard-driven escape path still calls the refocus code so keyboard dismissals
behave unchanged.

In `@tedi/components/overlay/dropdown/dropdown.component.spec.ts`:
- Around line 69-72: The afterEach teardown currently calls
dropdown.hideDropdown() and clears overlayContainerElement.innerHTML but does
not restore Jest timers; add a call to jest.useRealTimers() in the same
afterEach to ensure any tests that switched to fake timers (e.g., those using
fakeAsync/tick) cannot leak a fake clock into later tests — update the afterEach
block that invokes dropdown.hideDropdown() and overlayContainerElement.innerHTML
to also call jest.useRealTimers().

In `@tedi/components/overlay/dropdown/dropdown.component.ts`:
- Around line 103-106: The onOutsideClick handler currently calls
this.hideDropdown() and then unconditionally focuses the trigger element, which
reverts focus when the user clicks another control; remove the call to
this.dropdownTrigger().host.nativeElement.focus() from onOutsideClick so outside
(pointer) clicks simply close the overlay without restoring focus, and leave
keyboard dismissal/focus restoration to the existing Escape/trigger directive
logic (verify the escape handler in the trigger directive retains its focus
behavior). Ensure no other callers rely on onOutsideClick to restore focus and
update any related tests to reflect pointer-driven outside clicks should not
move focus.

In `@tedi/components/overlay/tooltip/tooltip.component.ts`:
- Around line 108-113: The showTooltip() implementation must always cancel any
pending hide timer before attempting to re-open; move or perform
clearTimeout(this.hideTimeout) unconditionally at the start of showTooltip()
(before or independent of the this.isOpen() check) so that an armed hideTimeout
cannot remove the tooltip after re-showing; update the method that references
showTooltip, this.hideTimeout, and this.isOpen to ensure the timer is cleared
every time showTooltip is called.

---

Outside diff comments:
In `@tedi/components/overlay/dropdown/dropdown.component.ts`:
- Around line 2-10: The component's self-provider in dropdown.component.ts must
be wrapped with Angular's forwardRef to defer the class reference; update the
providers entry that references the component itself (e.g., where
DropdownComponent is provided for NG_VALUE_ACCESSOR or DROPDOWN_CONTENT_API) to
use forwardRef(() => DropdownComponent), and add import { forwardRef } from
'@angular/core' if missing so the decorator metadata no longer references the
class before it's defined.

In `@tedi/components/overlay/popover/popover.component.ts`:
- Around line 147-179: Store the boolean values used at open time instead of
re-reading reactive signals on close and to handle dynamic inputs: in
showPopover() capture and save this._lockedScrollAtOpen = this.lockScroll(),
this._hideOnScrollAtOpen = this.hideOnScroll(), and this._dismissibleAtOpen =
this.dismissible(); use _lockedScrollAtOpen in hidePopover() to decide whether
to restore document.body overflow; in onOverlayAttach() use the captured
_hideOnScrollAtOpen and _dismissibleAtOpen to decide whether to call
setupScrollListener() and setupDismissListeners(), and add an effect() that
watches lockScroll(), hideOnScroll(), and dismissible() to attach or detach
listeners via setupScrollListener()/teardownScrollListener() and
setupDismissListeners()/teardownDismissListeners() so listeners are reconciled
while the popover is open (refer to showPopover(), hidePopover(),
onOverlayAttach(), setupScrollListener(), setupDismissListeners()).
- Around line 158-180: The focus restoration routine can pick a focusable
element inside the still-attached overlay and later call focus() on a detached
node; update focusElementAfterTrigger() and focusElementBeforeTrigger() to
exclude overlay descendants when computing
getFocusableElements(this.document.body) (filter out elements contained in
this.connectedOverlay()?.overlayRef?.overlayElement or the
".tedi-popover__container") before picking the next/previous target, or
alternatively compute the target after hidePopover() completes; ensure
hidePopover(), focusElementAfterTrigger(), and focusElementBeforeTrigger() are
updated to use this filtered list so focus() never targets detached overlay
elements.

---

Duplicate comments:
In @.claude/skills/contributing/references/refactoring.md:
- Around line 43-46: Update "Step 5: Code Review" to document the /simplify
command and define "valid findings": add a short description of what /simplify
does and where it runs (CLI/IDE/web), show the exact invocation or UI steps to
execute it, describe expected output format (types of findings, severity levels,
examples), and provide clear criteria for marking findings as "valid" (e.g.,
correctness, performance, readability, duplication) plus a link to further docs
or help; reference the Step 5 header and the literal "/simplify" command so
reviewers can find and expand the current text.

---

Nitpick comments:
In `@tedi/components/layout/header/header-language/header-language.component.ts`:
- Around line 3-12: Replace the `@ViewChild` decorator-based popover query with a
signal-based query: remove the ViewChild import and the `@ViewChild`(...) popover
property, add a signal like popoverRef = signal<HTMLElement | null>(null) (or
the appropriate component type), and ensure the template uses a template
reference variable (e.g. `#popover`) and the component sets popoverRef (e.g. in
ngAfterViewInit or via a small template binding/handler) so all usages that
referenced the old popover property now read from popoverRef() (or computed()
wrappers) — update any code that accessed the old popover symbol to use the new
popoverRef signal.

In `@tedi/components/overlay/overlay-position.util.ts`:
- Around line 184-195: Change the loose string typing to a strict placement
union so only the four resolved sides are allowed: update
getPlacementFromPositionChange to return a union type ('top' | 'bottom' | 'left'
| 'right') instead of string, and narrow any callers/related helpers (notably
calculateArrowOffset) to accept that same placement union; ensure
getPlacementFromPositionChange maps
ConnectedOverlayPositionChange.connectionPair values into that union only and
update the arrow helper signatures (and any other usages in the 272-308 range)
to use the new placement type so invalid values like "auto" or "top-start" are
rejected at compile time.

In `@tedi/components/overlay/popover/popover.component.spec.ts`:
- Around line 205-210: Tests call component.onOverlayAttach() directly which
bypasses the template-driven CdkConnectedOverlay.attach event; instead locate
the CdkConnectedOverlay directive instance in the test (query the overlay
directive from the fixture), and drive the lifecycle by emitting its attach
output (e.g., overlayDirective.attach.emit()) or by opening the popover with
component.showPopover() and asserting the attach emitter is fired; replace
direct onOverlayAttach() calls with this event-driven emit in the affected specs
(references: component.showPopover(), onOverlayAttach(),
CdkConnectedOverlay.attach); keep one focused unit test that directly calls
onOverlayAttach() only if you want to verify its internal behavior separately.
🪄 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: b73c4d35-8d02-4c78-b978-d4dc7b026c9e

📥 Commits

Reviewing files that changed from the base of the PR and between a745686 and b8bd51e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (40)
  • .claude/skills/contributing/references/new-component.md
  • .claude/skills/contributing/references/refactoring.md
  • .github/workflows/angular-test-and-lint.yml
  • .stylelintrc.json
  • README.md
  • package.json
  • skills/tedi-angular/SKILL.md
  • skills/tedi-angular/references/components.md
  • tedi/components/form/date-picker/date-picker.component.html
  • tedi/components/form/date-picker/date-picker.component.spec.ts
  • tedi/components/form/date-picker/date-picker.component.ts
  • tedi/components/form/filter/filter.component.html
  • tedi/components/form/filter/filter.component.spec.ts
  • tedi/components/form/filter/filter.component.ts
  • tedi/components/form/filter/filter.stories.ts
  • tedi/components/layout/header/header-language/header-language.component.html
  • tedi/components/layout/header/header-language/header-language.component.ts
  • tedi/components/layout/header/header-profile/header-profile.component.html
  • tedi/components/layout/header/header-role/header-role.component.html
  • tedi/components/overlay/dropdown/dropdown-trigger/dropdown-trigger.directive.ts
  • tedi/components/overlay/dropdown/dropdown.component.html
  • tedi/components/overlay/dropdown/dropdown.component.scss
  • tedi/components/overlay/dropdown/dropdown.component.spec.ts
  • tedi/components/overlay/dropdown/dropdown.component.ts
  • tedi/components/overlay/dropdown/dropdown.stories.ts
  • tedi/components/overlay/dropdown/dropdown.tokens.ts
  • tedi/components/overlay/index.ts
  • tedi/components/overlay/overlay-position.util.ts
  • tedi/components/overlay/popover/popover-content/popover-content.component.spec.ts
  • tedi/components/overlay/popover/popover-trigger/popover-trigger.directive.ts
  • tedi/components/overlay/popover/popover.component.html
  • tedi/components/overlay/popover/popover.component.scss
  • tedi/components/overlay/popover/popover.component.spec.ts
  • tedi/components/overlay/popover/popover.component.ts
  • tedi/components/overlay/tooltip/tooltip-trigger/tooltip-trigger.component.ts
  • tedi/components/overlay/tooltip/tooltip.component.html
  • tedi/components/overlay/tooltip/tooltip.component.scss
  • tedi/components/overlay/tooltip/tooltip.component.spec.ts
  • tedi/components/overlay/tooltip/tooltip.component.ts
  • tedi/components/overlay/tooltip/tooltip.stories.ts
💤 Files with no reviewable changes (2)
  • skills/tedi-angular/SKILL.md
  • tedi/components/overlay/popover/popover-content/popover-content.component.spec.ts

Comment on lines +57 to +60
## Step 6: Code Review

Run `/simplify` to review all changed code for reuse opportunities, code quality issues, and efficiency problems. Fix all valid findings before proceeding.

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

Document the /simplify command and clarify "valid findings".

The new code review step introduces the /simplify command without explanation. Users need to know:

  • What the /simplify command is and what it does
  • Where/how to execute it (CLI, IDE plugin, web interface, etc.)
  • What kind of output to expect
  • How to determine which findings are "valid" (criteria, judgment process)

Consider adding a brief explanation or a reference link to documentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/contributing/references/new-component.md around lines 57 -
60, Update "Step 6: Code Review" to document the /simplify command and define
"valid findings": add a short description of what /simplify does (e.g.,
automated code-review assistant that suggests reuse, quality, and efficiency
improvements), where to run it (CLI command or IDE/web integration) and what
output looks like (summary of issues, severity levels, suggested fixes, and
example output). Also add criteria for "valid findings" (e.g., reproducible
issues, non-whitespace changes, security/bug/performance regressions, and
clearly actionable suggestions) and a short guidance on how to triage
suggestions (accept, request changes, or ignore with reason); include a
reference link to the tool/docs if available and mention the command name
"/simplify" and the Step heading ("Step 6: Code Review") so reviewers can find
and run it.

Comment thread .stylelintrc.json Outdated
Comment on lines 9 to 11
"^((tedi|cdk)-[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:__[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*(?:--[a-z][a-z0-9]+(?:-[a-z0-9]+)*)?|ng-[a-z]+(?:-[a-z]+)*)$",
{
"message": "Class selector must start with 'tedi-' prefix and follow BEM naming (e.g., .tedi-button, .tedi-button__icon, .tedi-button--primary). Selector: \"%s\"",
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

Align lint error message with allowed prefixes

Line 9 allows tedi-, cdk-, and ng-, but Line 11 says selectors must start with tedi-. Please update the message to reflect the actual accepted patterns; otherwise lint feedback is misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.stylelintrc.json around lines 9 - 11, The lint message for the class
selector rule incorrectly states selectors must start with 'tedi-' even though
the regex pattern "^((tedi|cdk)-...|ng-... )$" allows 'tedi-', 'cdk-' and 'ng-'
prefixes; update the message string (the second item paired with that regex) to
list all allowed prefixes and keep the BEM guidance (e.g., "Class selector must
start with 'tedi-', 'cdk-' or 'ng-' prefix and follow BEM naming (e.g.,
.tedi-button, .cdk-modal, .ng-component, .tedi-button__icon,
.tedi-button--primary). Selector: \"%s\"").

Comment on lines 66 to +77
private openAndFocusFirst() {
if (!this.dropdown.floatUiComponent().state) {
if (!this.dropdown.isOpen()) {
this.dropdown.showDropdown();
}
this.dropdown.focusFirstItem?.();
setTimeout(() => this.dropdown.focusFirstItem?.());
}

private openAndFocusLast() {
if (!this.dropdown.floatUiComponent().state) {
if (!this.dropdown.isOpen()) {
this.dropdown.showDropdown();
}
this.dropdown.focusLastItem?.();
setTimeout(() => this.dropdown.focusLastItem?.());
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

Avoid a second deferred focus jump on keyboard open.

DropdownComponent.showDropdown() already queues focusActiveItem() after opening in tedi/components/overlay/dropdown/dropdown.component.ts:74-85. These extra timers add a competing focus change, so ArrowUp/ArrowDown can briefly focus one item and then immediately jump to another. Push the initial-focus choice into the dropdown component (or suppress its default post-open focus for this path) so only one focus pass runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tedi/components/overlay/dropdown/dropdown-trigger/dropdown-trigger.directive.ts`
around lines 66 - 77, openAndFocusFirst and openAndFocusLast call showDropdown
then schedule an extra setTimeout-based focus (focusFirstItem/focusLastItem)
which conflicts with DropdownComponent.showDropdown's own queued
focusActiveItem; remove the competing timer and instead add an option to
DropdownComponent.showDropdown (e.g., showDropdown({ initialFocus?:
'auto'|'first'|'last' })) or a boolean flag to suppress its default post-open
focusActiveItem so the caller can request first/last focus without a second
deferred focus; update openAndFocusFirst/openAndFocusLast to call showDropdown
with the new option (and remove the setTimeout calls) or, alternatively, expose
a parameter to skip the internal focus and let these methods perform the single
focus pass.

Comment on lines +3 to +7
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="overlayOrigin()"
[cdkConnectedOverlayOpen]="isOpen()"
[cdkConnectedOverlayPositions]="overlayPositions()"
(overlayOutsideClick)="onOutsideClick()"
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

Don't steal focus on pointer-driven outside clicks.

Line 7 sends every outside click through onOutsideClick(), and that handler refocuses the trigger (tedi/components/overlay/dropdown/dropdown.component.ts:87-106). Clicking another input or button will therefore close the dropdown and then move focus back to the trigger, which breaks normal pointer interaction outside the menu. Outside clicks should dismiss only; keep trigger refocus for keyboard dismissals such as Escape. The matching focus assertion in the spec will need to be relaxed too.

💡 Minimal fix
-  (overlayOutsideClick)="onOutsideClick()"
+  (overlayOutsideClick)="hideDropdown()"
📝 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
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="overlayOrigin()"
[cdkConnectedOverlayOpen]="isOpen()"
[cdkConnectedOverlayPositions]="overlayPositions()"
(overlayOutsideClick)="onOutsideClick()"
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="overlayOrigin()"
[cdkConnectedOverlayOpen]="isOpen()"
[cdkConnectedOverlayPositions]="overlayPositions()"
(overlayOutsideClick)="hideDropdown()"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/dropdown/dropdown.component.html` around lines 3 - 7,
The outside-click handler currently always refocuses the trigger via
onOutsideClick(), breaking pointer interaction; change the template to pass the
event to the handler (overlayOutsideClick)="onOutsideClick($event)" and update
dropdown.component.ts onOutsideClick(event) to only refocus the trigger when the
dismissal is from keyboard (e.g., event is a KeyboardEvent or event.key ===
'Escape') and for pointer-driven events (MouseEvent) simply close the menu
without moving focus; keep existing close logic and ensure any keyboard-driven
escape path still calls the refocus code so keyboard dismissals behave
unchanged.

Comment on lines +69 to 72
afterEach(() => {
dropdown.hideDropdown();
overlayContainerElement.innerHTML = "";
});
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

Restore real timers in teardown.

Several tests later in this file switch to fake timers, for example on Lines 170-177 and Lines 419-480. If any of them fail before jest.useRealTimers(), the suite inherits the fake clock because afterEach() does not reset it. Restoring timers centrally here makes the spec self-healing.

💡 Minimal fix
  afterEach(() => {
+    jest.useRealTimers();
     dropdown.hideDropdown();
     overlayContainerElement.innerHTML = "";
  });
As per coding guidelines, "Clean up `fakeAsync`/`tick` usage in `afterEach` in test files".
📝 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
afterEach(() => {
dropdown.hideDropdown();
overlayContainerElement.innerHTML = "";
});
afterEach(() => {
jest.useRealTimers();
dropdown.hideDropdown();
overlayContainerElement.innerHTML = "";
});
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 70-70: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: overlayContainerElement.innerHTML = ""
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/dropdown/dropdown.component.spec.ts` around lines 69
- 72, The afterEach teardown currently calls dropdown.hideDropdown() and clears
overlayContainerElement.innerHTML but does not restore Jest timers; add a call
to jest.useRealTimers() in the same afterEach to ensure any tests that switched
to fake timers (e.g., those using fakeAsync/tick) cannot leak a fake clock into
later tests — update the afterEach block that invokes dropdown.hideDropdown()
and overlayContainerElement.innerHTML to also call jest.useRealTimers().

Comment on lines +103 to +106
onOutsideClick() {
this.hideDropdown();
this.dropdownTrigger().host.nativeElement.focus();
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the dropdown component file
cat -n tedi/components/overlay/dropdown/dropdown.component.ts | head -150

Repository: TEDI-Design-System/angular

Length of output: 4964


🏁 Script executed:

# Let's check the dropdown template to see how onOutsideClick is connected
cat -n tedi/components/overlay/dropdown/dropdown.component.html

Repository: TEDI-Design-System/angular

Length of output: 624


🏁 Script executed:

# Check the dropdown trigger directive to see keyboard handling
cat -n tedi/components/overlay/dropdown/dropdown-trigger/dropdown-trigger.directive.ts

Repository: TEDI-Design-System/angular

Length of output: 2825


Remove unconditional focus restoration from pointer-driven outside clicks.

When overlayOutsideClick fires (user clicks outside the overlay), unconditionally calling focus() on the trigger bounces focus back if the user clicked another button or input. This is a noticeable accessibility regression. Keep focus restoration only for keyboard dismissal (Escape key), which is already handled in the trigger directive.

Suggested fix
   onOutsideClick() {
     this.hideDropdown();
-    this.dropdownTrigger().host.nativeElement.focus();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/dropdown/dropdown.component.ts` around lines 103 -
106, The onOutsideClick handler currently calls this.hideDropdown() and then
unconditionally focuses the trigger element, which reverts focus when the user
clicks another control; remove the call to
this.dropdownTrigger().host.nativeElement.focus() from onOutsideClick so outside
(pointer) clicks simply close the overlay without restoring focus, and leave
keyboard dismissal/focus restoration to the existing Escape/trigger directive
logic (verify the escape handler in the trigger directive retains its focus
behavior). Ensure no other callers rely on onOutsideClick to restore focus and
update any related tests to reflect pointer-driven outside clicks should not
move focus.

Comment on lines 108 to 113
showTooltip() {
if (!this.floatUiComponent().state) {
if (!this.isOpen()) {
clearTimeout(this.hideTimeout);
this.floatUiComponent().show();
this.floatUiDisplay.set("block");
this.isOpen.set(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

Always clear the pending hide timer before re-showing.

Line 109 makes clearTimeout(this.hideTimeout) conditional on the tooltip being closed. If pointer or focus re-enters while a hide timeout is already armed, showTooltip() leaves that timer running and the tooltip can still disappear underneath the user even though it is being shown again.

💡 Minimal fix
  showTooltip() {
+    clearTimeout(this.hideTimeout);
     if (!this.isOpen()) {
-      clearTimeout(this.hideTimeout);
       this.isOpen.set(true);
     }
  }
📝 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
showTooltip() {
if (!this.floatUiComponent().state) {
if (!this.isOpen()) {
clearTimeout(this.hideTimeout);
this.floatUiComponent().show();
this.floatUiDisplay.set("block");
this.isOpen.set(true);
}
}
showTooltip() {
clearTimeout(this.hideTimeout);
if (!this.isOpen()) {
this.isOpen.set(true);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tedi/components/overlay/tooltip/tooltip.component.ts` around lines 108 - 113,
The showTooltip() implementation must always cancel any pending hide timer
before attempting to re-open; move or perform clearTimeout(this.hideTimeout)
unconditionally at the start of showTooltip() (before or independent of the
this.isOpen() check) so that an armed hideTimeout cannot remove the tooltip
after re-showing; update the method that references showTooltip,
this.hideTimeout, and this.isOpen to ensure the timer is cleared every time
showTooltip is called.

…loat-ui

# Conflicts:
#	tedi/components/form/filter/filter.component.ts
#	tedi/components/form/filter/filter.stories.ts
#	tedi/components/helpers/scroll-fade/scroll-fade.component.scss
#	tedi/components/helpers/scroll-fade/scroll-fade.component.ts
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