Skip to content

Conversation

@BryanCunningham
Copy link
Contributor

@BryanCunningham BryanCunningham commented Oct 30, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/CL-736

📔 Objective

Update chip-select component to use signal based inputs

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@github-actions
Copy link
Contributor

github-actions bot commented Oct 30, 2025

Logo
Checkmarx One – Scan Summary & Detailsf97ff966-d2c5-4999-98d5-bf76ad135822

Great job! No new security vulnerabilities introduced in this pull request

@codecov
Copy link

codecov bot commented Oct 30, 2025

Codecov Report

❌ Patch coverage is 89.47368% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.76%. Comparing base (7989ad7) to head (1ab1a24).
⚠️ Report is 20 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...omponents/src/chip-select/chip-select.component.ts 89.47% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #17136      +/-   ##
==========================================
+ Coverage   40.70%   40.76%   +0.05%     
==========================================
  Files        3541     3541              
  Lines      101404   101411       +7     
  Branches    15186    15187       +1     
==========================================
+ Hits        41275    41336      +61     
+ Misses      58386    58329      -57     
- Partials     1743     1746       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BryanCunningham BryanCunningham marked this pull request as ready for review November 3, 2025 14:48
@BryanCunningham BryanCunningham requested a review from a team as a code owner November 3, 2025 14:48
@BryanCunningham BryanCunningham changed the title migrate chip select to use signals [CL-736] migrate chip select to use signals Nov 3, 2025
willmartian
willmartian previously approved these changes Nov 3, 2025
@claude
Copy link

claude bot commented Nov 3, 2025

Claude finished @BryanCunningham's task —— View job


Code Review Complete

Summary

This PR successfully migrates the chip-select component to use Angular signals per ADR-0027. The signal inputs, viewChild queries, and computed disabled state are well-implemented. Recent commits show good responsiveness to feedback with test consolidation and documentation improvements.

Key strengths:

  • Clean signal input migration with proper transformations
  • Well-designed dual-signal disabled state pattern (template + programmatic)
  • Effect-based initialization replaces lifecycle hooks
  • Good explanatory comment for markForCheck() necessity

Areas needing attention:


Findings

Finding 1: Partial Signal Migration Creates Hybrid Pattern

⚠️ libs/components/src/chip-select/chip-select.component.ts:48

OnPush change detection was added, but several state properties remain as class properties instead of signals: selectedOption (line 128), renderedOptions (line 125), and menuWidth (line 134).

Why this matters: With OnPush, changes to these properties won't automatically trigger change detection. The current implementation works because:

  • User events (clicks) automatically trigger CD
  • markForCheck() is called in writeValue()

Risk: This pattern is fragile. Future code paths that modify these properties (e.g., programmatic API calls to clear(), selectOption(), etc.) may not trigger view updates if called outside Angular's zone.

Consider completing the signal migration
protected readonly selectedOption = signal<ChipSelectOption<T> | null>(null);
protected readonly renderedOptions = signal<ChipSelectOption<T> | null>(null);
protected readonly menuWidth = signal<number | null>(null);

protected readonly label = computed(() => 
  this.selectedOption()?.label || this.placeholderText()
);
protected readonly icon = computed(() => 
  this.selectedOption()?.icon || this.placeholderIcon()
);

This would eliminate the need for markForCheck() and make the component fully reactive, aligning better with ADR-0027's intent.

Alternative: If keeping the hybrid approach, document which methods are safe to call programmatically vs. which require Angular context.


Finding 2: CSS Class Testing Remains After Agreed Removal

🎨 libs/components/src/chip-select/chip-select.component.spec.ts:78-81, 310-312

Despite vleague2's feedback about avoiding CSS class tests and your agreement to "rip this out," icon element tests still check for presence rather than just verifying an icon exists:

// Line 78-81
const icon = fixture.debugElement.query(By.css('i[aria-hidden="true"]'));
expect(icon).toBeTruthy();

// Line 310-312  
const icon = fixture.debugElement.query(By.css('i[aria-hidden="true"]'));
expect(icon).toBeTruthy();

While these don't test the specific bwi-* classes anymore (good!), they still couple to the implementation detail that icons use <i> tags with aria-hidden="true".

Better approach: Test that an icon is visually present to the user or that the icon input was properly bound, without asserting DOM structure. However, given the recent test consolidation work, this is a minor improvement rather than a blocking issue.


Finding 3: Effect Dependency Pattern is Intentional

💭 libs/components/src/chip-select/chip-select.component.ts:115-117

The comment added (lines 115-117) correctly explains why isOpen is accessed outside signal tracking. This is the right pattern and the comment makes the intention clear. Well done.


Finding 4: Test Coverage Gap

💭 Coverage report shows 89.47% with 2 lines missing

According to Codecov, 2 lines lack coverage. While not blocking, consider identifying which lines are uncovered to determine if they represent important edge cases.


Recommendation

This is solid work. The remaining issues are improvements rather than blockers:

  • Finding 1 (hybrid pattern) is the most significant - consider completing the signal migration for consistency and maintainability
  • Finding 2 (icon tests) is minor given the test cleanup already done
  • Findings 3-4 are observations rather than action items

The PR achieves its objective of migrating to signal inputs. Whether to complete the full signal migration for internal state is a team decision about consistency vs. incremental migration.


writeValue(obj: T): void {
this.selectedOption = this.findOption(this.rootTree, obj);
this.setOrResetRenderedOptions();
this.cdr.markForCheck();
Copy link

Choose a reason for hiding this comment

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

💭 Re: vleague2's question "Why do we need this now?"

This markForCheck() call is necessary because OnPush change detection was added (line 48). When writeValue() is called programmatically by Angular forms (external to the component), OnPush components don't automatically detect changes to component properties. Without this call, the view wouldn't update when the form control's value changes.

This is the correct pattern for CVA implementations with OnPush.

});

describe("Signal Inputs", () => {
it("should initialize with required signal inputs", () => {
Copy link

Choose a reason for hiding this comment

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

💭 Re: vleague2's feedback about test coverage

Agree that the "Signal Inputs" test block tests Angular framework behavior rather than user-facing behavior. These tests will require updates if implementation details change (e.g., switching from signals to another reactive primitive).

Suggested refinement: Focus tests on observable behavior and user interactions rather than internal signal mechanics. Tests like "should initialize with required signal inputs" and "should update when options signal changes" verify that Angular's signal system works, not that this component works correctly.

Consider keeping only tests that verify:

  • User interactions produce expected results
  • Form integration works correctly
  • Accessibility features function properly
  • Edge cases are handled

multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
Copy link

Choose a reason for hiding this comment

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

⚠️ OnPush compatibility issue with non-signal state

Finding 2: Adding OnPush change detection improves performance, but several template-bound properties are not signals: selectedOption, renderedOptions, menuWidth, and the label/icon getters that depend on them.

With OnPush, these properties won't automatically trigger view updates when changed through component methods like selectOption(), clear(), viewOption(), etc.

Current workarounds that make this work:

  • User events (clicks) trigger change detection automatically
  • markForCheck() in writeValue() handles form updates

Risk: This creates a fragile pattern where some code paths trigger detection and others might not. Consider converting these to signals for consistency:

Suggested refactor
protected readonly selectedOption = signal<ChipSelectOption<T> | null>(null);
protected readonly renderedOptions = signal<ChipSelectOption<T> | null>(null);
protected readonly menuWidth = signal<number | null>(null);

protected readonly label = computed(() => 
  this.selectedOption()?.label || this.placeholderText()
);
protected readonly icon = computed(() => 
  this.selectedOption()?.icon || this.placeholderIcon()
);

This would eliminate the need for markForCheck() and make the component fully reactive.

const currentMenu = this.menu();
const trigger = this.menuTrigger();
// Only focus if menu is actually open to prevent running during initialization
if (items.length > 0 && trigger?.isOpen) {
Copy link

Choose a reason for hiding this comment

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

💭 Effect dependency consideration

The effect tracks menuItems(), menu(), and menuTrigger() but checks trigger?.isOpen in the condition. Since isOpen is not a signal, changes to it won't trigger this effect.

Current behavior: Effect runs when menuItems change (e.g., navigating submenus) and only focuses if menu happens to be open at that moment.

This appears intentional and correct - you want to focus the first item when navigating between submenus, not every time isOpen changes. The previous menuItems.changes subscription had the same behavior.

Consider adding a comment to clarify this is intentional:

// Only focus if menu is open; isOpen is deliberately not tracked
// to avoid re-focusing when menu opens/closes

private readonly disabledState = signal(false);

/** Combined disabled state from both input and programmatic control */
readonly disabled = computed(() => this.disabledInput() || this.disabledState());
Copy link

Choose a reason for hiding this comment

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

🎨 Well-designed disabled state pattern

The dual-signal approach for handling disabled state is excellent:

  • disabledInput - template binding via [disabled] attribute
  • disabledState - programmatic control via setDisabledState() (CVA)
  • disabled - computed combination of both

This properly handles both use cases and prevents conflicts between template and form control disabled states. Good pattern for CVA + signal migrations.

Comment on lines 514 to 515
const currentValue = formComponent.form.value.selection;
if (currentValue && !chipComponent["selectedOption"]) {
Copy link

Choose a reason for hiding this comment

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

🎨 Test setup workaround indicates fragile pattern

This conditional writeValue call in beforeEach is unusual - it suggests the component isn't properly initializing from the form control's initial value. In real usage with Angular forms, CVA's writeValue should be called automatically by the framework.

This workaround makes the test less representative of actual usage and may mask initialization issues.

@BryanCunningham BryanCunningham merged commit ccf7bb1 into main Nov 13, 2025
120 checks passed
@BryanCunningham BryanCunningham deleted the uif/CL-736/chip-select-signals branch November 13, 2025 20:53
maxkpower pushed a commit that referenced this pull request Nov 17, 2025
* migrate chip select to use signals

* Have Claude address feedback and create spec file

* remove eslint disable comment

* fix failing tests

* remove unnecessary tests

* improved documentation

* remove unnecessary test logic

* consolidate tests and remove fragile selectors
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.

4 participants