Skip to content

[CLOV-1337][BpkButton] Add leadingIcon and trailingIcon props#4269

Open
Richard-Shen (RichardSyq) wants to merge 14 commits intomainfrom
CLOV-1337_bpk_button_leading_trailing_icon_props
Open

[CLOV-1337][BpkButton] Add leadingIcon and trailingIcon props#4269
Richard-Shen (RichardSyq) wants to merge 14 commits intomainfrom
CLOV-1337_bpk_button_leading_trailing_icon_props

Conversation

@RichardSyq
Copy link
Contributor

@RichardSyq Richard-Shen (RichardSyq) commented Mar 10, 2026

Summary

Adds leadingIcon and trailingIcon optional props to BpkButtonV2, providing dedicated icon slots before and after the button label.

Previously, icons had to be passed via children with manual alignment using withButtonAlignment(). The new props offer a cleaner API with automatic flexbox alignment, consistent spacing, and no wrapper needed.

Changes

common-types.tsx — added leadingIcon?: ReactNode and trailingIcon?: ReactNode to Props
BpkButton.tsx — renders icon wrapper spans, applies bpk-button--has-icon class when icons are present
BpkButton.module.scss — flexbox layout with gap for spacing; text-decoration: none on icon wrappers to prevent underline bleeding on link buttons
BpkButton.figma.tsx — updated Code Connect to use figma.boolean for leadingIcon, trailingIcon and iconOnly
BpkButton-test.tsx — DOM assertion tests for icon prop variants
examples/ — all button type stories updated to use leadingIcon/trailingIcon props; removed old children-based icon pattern

Remember to include the following changes:

  • Ensure the PR title includes the name of the component you are changing so it's clear in the release notes for consumers of the changes in the version e.g [Clover-123][BpkButton] Updating the colour
  • README.md (If you have created a new component)
  • Component README.md
  • Tests
  • Accessibility tests
    • The following checks were performed:
      • Ability to navigate using a keyboard only
      • Zoom functionality (Deque University explanation):
        • The page SHOULD be functional AND readable when only the text is magnified to 200% of its initial size
        • Pages must reflow as zoom increases up to 400% so that content continues to be presented in only one column i.e. Content MUST NOT require scrolling in two directions (both vertically and horizontally)
      • Ability to navigate using a screen reader only
  • Storybook examples created/updated
  • For breaking changes or deprecating components/properties, migration guides added to the description of the PR. If the guide has large changes, consider creating a new Markdown page inside the component's docs folder and link it here

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 10, 2026 10:40
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

Copy link
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

Adds support for optional leading and trailing icons to BpkButtonV2, including styling and Storybook/Figma examples, plus updated tests to cover the new API.

Changes:

  • Extend BpkButtonV2 props with leadingIcon and trailingIcon, and render them with new BEM element wrappers.
  • Add styling for icon layout (--has-icon, __leading-icon, __trailing-icon) and full-width alignment.
  • Update accessibility/unit tests and add Storybook + Figma Code Connect examples for the new icon props.

Reviewed changes

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

Show a summary per file
File Description
packages/bpk-component-button/src/BpkButtonV2/common-types.tsx Adds leadingIcon/trailingIcon to the component props type.
packages/bpk-component-button/src/BpkButtonV2/BpkButton.tsx Renders optional leading/trailing icons and applies a --has-icon modifier.
packages/bpk-component-button/src/BpkButtonV2/BpkButton.module.scss Adds layout rules for icon-bearing buttons and icon wrappers.
packages/bpk-component-button/src/BpkButtonV2/accessibility-test.tsx Adds axe coverage for leading/trailing icon configurations.
packages/bpk-component-button/src/BpkButtonV2/BpkButton-test.tsx Adds unit tests for icon rendering and class application (incl. snapshots).
packages/bpk-component-button/src/BpkButtonV2/BpkButton.figma.tsx Updates Code Connect props to use leadingIcon/trailingIcon.
examples/bpk-component-button/stories.tsx Adds new Storybook stories/visual test entries for icon props.
examples/bpk-component-button/examples.tsx Adds Storybook example components that exercise the new icon props.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Comment on lines +119 to +122
&--full-width#{&}--has-icon {
display: flex;
justify-content: center;
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The selector &--full-width#{&}--has-icon works, but the interpolation makes it harder to read/maintain. This can be expressed more clearly as &--full-width&--has-icon (same combined selector) while avoiding string interpolation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current version of sass-loader does not support &--full-width&--has-icon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

@skyscanner-backpack-bot
Copy link

skyscanner-backpack-bot bot commented Mar 10, 2026

Warnings
⚠️

Package source files (e.g. packages/package-name/src/Component.js) were updated, but snapshots weren't. Have you checked that the tests still pass?

Browser support

If this is a visual change, make sure you've tested it in multiple browsers.

Generated by 🚫 dangerJS against 4c4af47

Richard Shen and others added 3 commits March 11, 2026 11:15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

…railingIcon props

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

- Use figma.boolean for leadingIcon, trailingIcon and iconOnly props
- Move leadingIcon/trailingIcon adjacent in props destructuring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

Copy link
Contributor Author

@RichardSyq Richard-Shen (RichardSyq) left a comment

Choose a reason for hiding this comment

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

Code review

Found 1 issue:

  1. Non-semantic spacing token used for icon-to-text gap — $bpk-spacing-icon-text: .5rem exists in backpack-foundations specifically for icon-to-text spacing and produces the same value as bpk-spacing-md(). Using the size-based function instead of the semantic variable breaks the principle stated in the Constitution and AGENTS.md: "Use semantic color tokens that describe purpose, not appearance" — the same applies to spacing. Replace gap: tokens.bpk-spacing-md() with gap: tokens.$bpk-spacing-icon-text.

&--has-icon {
display: inline-flex;
align-items: center;
gap: tokens.bpk-spacing-md();
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Copy link
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 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

Comment on lines +60 to 72
iconOnly: figma.boolean('Icon only')
},
example: ({ iconOnly, isDisabled, label, leftContent, rightContent, size, style }) => (
<BpkButton type={style} size={size} disabled={isDisabled} iconOnly={iconOnly}>
{leftContent}
example: ({ iconOnly, isDisabled, label, leadingIcon, size, style, trailingIcon }) => (
<BpkButton
type={style}
size={size}
disabled={isDisabled}
iconOnly={iconOnly}
leadingIcon={leadingIcon}
trailingIcon={trailingIcon}
>
{label}
{rightContent}
</BpkButton>
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In the Code Connect example, iconOnly is passed through but the button still renders {label} as children and doesn’t provide an accessible name. When iconOnly is true the rendered output will be an “icon-only styled” button that still contains text, and (if you switch to rendering only an icon) it would also need aria-label (e.g., derived from label). Consider branching the example so that iconOnly=true renders an icon as children and sets aria-label={label}, and also avoids passing leadingIcon/trailingIcon in that mode.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. The example is now branched on iconOnly:

When iconOnly=true: renders as children, sets aria-label={label}, and omits leadingIcon/trailingIcon

When iconOnly=false: renders {label} as children with leadingIcon/trailingIcon passed through as normal

@RichardSyq
Copy link
Contributor Author

Code review

Found 1 issue:

  1. Non-semantic spacing token used for icon-to-text gap — $bpk-spacing-icon-text: .5rem exists in backpack-foundations specifically for icon-to-text spacing and produces the same value as bpk-spacing-md(). Using the size-based function instead of the semantic variable breaks the principle stated in the Constitution and AGENTS.md: "Use semantic color tokens that describe purpose, not appearance" — the same applies to spacing. Replace gap: tokens.bpk-spacing-md() with gap: tokens.$bpk-spacing-icon-text.

&--has-icon {
display: inline-flex;
align-items: center;
gap: tokens.bpk-spacing-md();
}

🤖 Generated with Claude Code

  • If this code review was useful, please react with 👍. Otherwise, react with 👎.

However, no components under packages/ use $bpk-spacing-icon-text, so I think we should continue using bpk-spacing-md().

Richard Shen and others added 2 commits March 11, 2026 16:51
…Icon/trailingIcon

- README: add leadingIcon/trailingIcon usage examples
- button-link-type.md: update With Icons section to show new props as
  recommended approach; deprecate icon-as-children-sibling pattern;
  restore Custom Text Styling with BpkText with migration note
- BpkButton.figma.tsx: branch iconOnly example to render icon as
  children with aria-label, avoiding invalid leadingIcon/trailingIcon
  usage in icon-only mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

Copy link
Contributor

Choose a reason for hiding this comment

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

Implementation looks alright! Just a catch on hovering on the Link type button. (This is somewhat pre-existing, but would be good to resolve right here and now :D )

@gc-skyscanner
Copy link
Contributor

I assumed it is an issue in the case, could you help to fix it?

image

rel={rel}
{...rest}
>
{leadingIconEl}
Copy link
Contributor

Choose a reason for hiding this comment

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

&nbsp;
<Wrapped
size={SIZE_TYPES.large}
leadingIcon={<SmallLightningIcon />}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe we should use large icon here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks you reminder, I have modified this part.

@@ -35,16 +35,19 @@ export const BpkButtonV2 = ({
href = null,
iconOnly = false,
Copy link
Contributor

@gc-skyscanner GC Zhu (gc-skyscanner) Mar 12, 2026

Choose a reason for hiding this comment

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

Image

Sorry for being a bit annoying, do we need to handle the case where iconOnly=true but leadingIcon/trailingIcon are also passed? I tried it and it shows three icons. Is this intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good question! I'll consider this situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe the two newly added props should not affect the case where iconOnly=true. I've configured it so that leading icons and trailing icons are not rendered when iconOnly=true.

Choose a reason for hiding this comment

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

Is there a way to add typesafety where if iconOnly = true, then leading/trailing prop are not allowed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

- Use LargeLightningIcon for large button examples instead of small
- Fix link button hover animation to trigger from root button hover (not just text span) using CSS variables
- Ignore leadingIcon/trailingIcon when iconOnly=true to prevent three-icon rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

…er selectors

- Replace CSS variable approach with pure descendant hover selectors to avoid
  overriding dark mode underline styles
- Only apply hover animation override when icons are present (--has-icon)
- Remove link/linkOnDark anchor tag href from AnchorTagsExample to avoid
  pre-existing vertical centering issue with <a> tags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@RichardSyq
Copy link
Contributor Author

I assumed it is an issue in the case, could you help to fix it?

image

This is a pre-existing issue with large link buttons rendered as anchor tags ( via href). The base bpk-button mixin uses display: inline-block, which doesn't vertically center content in elements the way does natively.

I've solved it by removing href="#".

@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

RichardSyq

This comment was marked as duplicate.

@skyscanner-backpack-bot
Copy link

Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser.

Copy link
Contributor

Choose a reason for hiding this comment

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

One last thing and then this feels good to go!

rel={rel}
{...rest}
>
{leadingIconEl}

Choose a reason for hiding this comment

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

We should consider treating content for both link and button the same, should we extract it out and then put in? (Considering there's now conditional rendering for button but not for

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

Labels

ai: claude minor Non breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants