Skip to content

fix(pds-button): fix empty icon slot wrapper spacing#678

Merged
pixelflips merged 3 commits intomainfrom
fix/pds-button-empty-icon-slot-spacing
Feb 18, 2026
Merged

fix(pds-button): fix empty icon slot wrapper spacing#678
pixelflips merged 3 commits intomainfrom
fix/pds-button-empty-icon-slot-spacing

Conversation

@pixelflips
Copy link
Member

@pixelflips pixelflips commented Feb 17, 2026

Description

Fixes extra padding/spacing on buttons when no icon is slotted into the start/end slots.

PR #668 fixed a race condition by always rendering the <slot name="start"> and <slot name="end"> wrappers, using CSS :has(::slotted(*)) to hide them when empty. However, :has(::slotted(*)) has unreliable cross-browser support (particularly Safari). When the selector doesn't match, the empty .pds-button__icon spans remain display: inline-flex, and the parent's gap creates visible spacing where none should exist.

This replaces the CSS :has() approach with slotchange event handlers that toggle a --empty CSS class, following the existing pattern used in pds-input.

Fixes DSS-167

Screenshots

Before After
Screenshot 2026-02-17 at 9 49 41 AM Screenshot 2026-02-17 at 9 49 04 AM

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • unit tests
  • tested manually

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes

@netlify
Copy link

netlify bot commented Feb 17, 2026

Deploy Preview for pine-design-system ready!

Name Link
🔨 Latest commit af98820
🔍 Latest deploy log https://app.netlify.com/projects/pine-design-system/deploys/6994ae18bb3a92000845ef15
😎 Deploy Preview https://deploy-preview-678--pine-design-system.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.

@github-actions github-actions bot added bug Something isn't working package: core Changes have been made to the Core package labels Feb 17, 2026
@pixelflips pixelflips self-assigned this Feb 17, 2026
@pixelflips
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

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

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

The pds-button component’s automatic pseudo-class-based icon-hiding was replaced with an explicit .pds-button__icon--empty modifier in SCSS. The TypeScript component adds @State() flags hasStartContent and hasEndContent plus onslotchange handlers to track slot content and apply the --empty modifier to start/end icon containers. Tests and snapshots were updated to expect the pds-button__icon--empty modifier by default and to verify that it is removed when slot content is provided.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the fix: replacing unreliable CSS :has() logic with slotchange events to properly hide empty icon slot wrappers and eliminate unwanted spacing.
Description check ✅ Passed The description is well-structured with all required template sections completed: detailed motivation explaining the :has() browser support issue, type of change marked as bug fix, testing approach documented (unit and manual tests), and relevant checklist items completed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/pds-button-empty-icon-slot-spacing

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

Copy link

@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.

🧹 Nitpick comments (2)
libs/core/src/components/pds-button/pds-button.tsx (1)

160-166: Consider using assignedNodes({ flatten: true }) for nested slot handling.

The current implementation uses assignedNodes() without options, which won't include nodes from nested slots if the slotted content itself contains slots. While this is likely fine for the typical use case (slotting pds-icon elements), using { flatten: true } would be more robust.

Additionally, assignedNodes() includes text nodes (including whitespace), which could cause false positives. If only elements should be considered, assignedElements() might be more appropriate.

💡 Optional: Use assignedElements() for stricter element-only detection
  private handleStartSlotChange = (event: Event) => {
-   this.hasStartContent = (event.target as HTMLSlotElement).assignedNodes().length > 0;
+   this.hasStartContent = (event.target as HTMLSlotElement).assignedElements().length > 0;
  };

  private handleEndSlotChange = (event: Event) => {
-   this.hasEndContent = (event.target as HTMLSlotElement).assignedNodes().length > 0;
+   this.hasEndContent = (event.target as HTMLSlotElement).assignedElements().length > 0;
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/core/src/components/pds-button/pds-button.tsx` around lines 160 - 166,
The slot-change handlers handleStartSlotChange and handleEndSlotChange should
use flattened assignment and/or element-only detection to avoid nested slots and
text-node false positives; change the checks so they call
event.target.assignedElements({ flatten: true }) (or assignedNodes({ flatten:
true }) filtered for nodeType === Node.ELEMENT_NODE) and set
hasStartContent/hasEndContent based on the resulting element list length > 0.
libs/core/src/components/pds-button/test/pds-button.spec.tsx (1)

237-247: Snapshot shows --empty class despite content being slotted.

This test renders a button with <pds-icon slot="start"> slotted, but the snapshot still shows pds-button__icon--empty on the start wrapper. This appears inconsistent with the expected behavior—when content is slotted, the --empty class should be removed.

This is likely because newSpecPage doesn't automatically fire slotchange events, so hasStartContent remains false. While the behavior is technically correct for the test framework's limitations, the snapshot doesn't reflect real-world runtime behavior where the class would be absent.

Consider adding a comment explaining this limitation, or restructuring the test to simulate the slotchange event (as done in the new "slot empty class" test suite) to produce a more accurate snapshot.

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

In `@libs/core/src/components/pds-button/test/pds-button.spec.tsx` around lines
237 - 247, The snapshot shows pds-button__icon--empty because newSpecPage
doesn't fire slotchange so hasStartContent remains false; to fix, update the
test in pds-button.spec.tsx to simulate a slotchange for the start slot (as done
in the "slot empty class" suite) after rendering so the component updates and
removes pds-button__icon--empty, or alternatively add a short comment explaining
this test-framework limitation; locate the render using newSpecPage and the
component's slot handling (hasStartContent / slotchange) and either dispatch a
slotchange event and await changes or document why the snapshot intentionally
shows the --empty class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@libs/core/src/components/pds-button/pds-button.tsx`:
- Around line 160-166: The slot-change handlers handleStartSlotChange and
handleEndSlotChange should use flattened assignment and/or element-only
detection to avoid nested slots and text-node false positives; change the checks
so they call event.target.assignedElements({ flatten: true }) (or
assignedNodes({ flatten: true }) filtered for nodeType === Node.ELEMENT_NODE)
and set hasStartContent/hasEndContent based on the resulting element list length
> 0.

In `@libs/core/src/components/pds-button/test/pds-button.spec.tsx`:
- Around line 237-247: The snapshot shows pds-button__icon--empty because
newSpecPage doesn't fire slotchange so hasStartContent remains false; to fix,
update the test in pds-button.spec.tsx to simulate a slotchange for the start
slot (as done in the "slot empty class" suite) after rendering so the component
updates and removes pds-button__icon--empty, or alternatively add a short
comment explaining this test-framework limitation; locate the render using
newSpecPage and the component's slot handling (hasStartContent / slotchange) and
either dispatch a slotchange event and await changes or document why the
snapshot intentionally shows the --empty class.

@pixelflips
Copy link
Member Author

@coderabbitai i addressed the nitpick comments, review again.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

@pixelflips, I'll review the changes again now that you've addressed the nitpick comments.

✅ 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

@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.

🧹 Nitpick comments (1)
libs/core/src/components/pds-button/pds-button.tsx (1)

102-103: Initialize slot state on load to avoid first-render “empty” flicker.
hasStartContent/hasEndContent only update on slotchange. If content is present at initial render and the event is delayed or not fired in a target browser, the icon wrapper can stay hidden or flash. Consider setting initial state from light‑DOM slot nodes and keep slotchange for updates. Also confirm assignedElements({ flatten: true }) support in all target browsers (especially Safari versions in your support matrix).

♻️ Example initialization (before first render)
 export class PdsButton {
   `@Element`() el: HTMLPdsButtonElement;
@@
   `@State`() hasStartContent = false;
   `@State`() hasEndContent = false;
+
+  componentWillLoad() {
+    this.hasStartContent = !!this.el.querySelector('[slot="start"]');
+    this.hasEndContent = !!this.el.querySelector('[slot="end"]');
+  }

Also applies to: 160-166

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

In `@libs/core/src/components/pds-button/pds-button.tsx` around lines 102 - 103,
Initialize hasStartContent/hasEndContent from the light DOM before first render
in the PdsButton component (e.g., in componentWillLoad or connectedCallback) by
querying the named slots (slot[name="start"] and slot[name="end"]) and using
slot.assignedElements({ flatten: true }) to detect element children and fall
back to slot.assignedNodes() filtered for nodeType === Node.ELEMENT_NODE if
assignedElements isn't available; keep the existing slotchange handlers to
update state on changes and set hasStartContent/hasEndContent accordingly so
icons don't flicker on initial render, and consider adding a short comment about
browser support for assignedElements usage and the fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@libs/core/src/components/pds-button/pds-button.tsx`:
- Around line 102-103: Initialize hasStartContent/hasEndContent from the light
DOM before first render in the PdsButton component (e.g., in componentWillLoad
or connectedCallback) by querying the named slots (slot[name="start"] and
slot[name="end"]) and using slot.assignedElements({ flatten: true }) to detect
element children and fall back to slot.assignedNodes() filtered for nodeType ===
Node.ELEMENT_NODE if assignedElements isn't available; keep the existing
slotchange handlers to update state on changes and set
hasStartContent/hasEndContent accordingly so icons don't flicker on initial
render, and consider adding a short comment about browser support for
assignedElements usage and the fallback.

@pixelflips
Copy link
Member Author

@coderabbitai in regards to your latest nitpick comment:

The shadow DOM elements don't exist until after Stencil's first render() call, so we can't call slot.assignedElements() in componentWillLoad or connectedCallback — there's nothing to query yet.

The alternative (querying light DOM with querySelector('[slot="start"]')) is exactly the synchronous DOM check that PR #668 replaced due to race conditions withStencil's async task queue. Re-adding it would reintroduce the original bug (DSS-147).

The current approach defaults to hidden (--empty) and lets slotchange toggle visibility. This is the safe direction — slotchange fires during slot distribution before paint, so there's no visible flicker. A assignedElements fallback is also unnecessary since it shares the same browser support floor as shadow DOM itself.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Tip

For best results, initiate chat on the files or code changes.

@pixelflips, thank you for the detailed explanation! Your reasoning is sound:

  1. Lifecycle timing: You're absolutely right that shadow DOM slots aren't available in componentWillLoad or connectedCallback before the first render, making early assignedElements() checks impossible.

  2. Race condition avoidance: Querying light DOM directly would indeed reintroduce the synchronous DOM check issues that PR #668 solved.

  3. No flicker: Defaulting to --empty and letting slotchange toggle visibility is the safe direction since the event fires during slot distribution before paint.

Your approach is the correct solution given Stencil's rendering lifecycle and the constraints. I appreciate you taking the time to clarify the technical rationale!

@pixelflips pixelflips merged commit 86c3677 into main Feb 18, 2026
15 checks passed
@pixelflips pixelflips deleted the fix/pds-button-empty-icon-slot-spacing branch February 18, 2026 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working package: core Changes have been made to the Core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments