Skip to content

Commit c1669d2

Browse files
RajdeepcRajdeep Chandracaseyisonit
authored
fix(action-button): remove proxy click in capture phase (#5204)
* fix(action-button): remove proxy click in capture phase * fix(action-button): remove proxy click in capture phase keeping the form action * fix(action-button): added changeset * fix(button): removed proxy click and add meta key support for voiceover * fix(action-button): update changeset * chore: updated golden image cache --------- Co-authored-by: Rajdeep Chandra <[email protected]> Co-authored-by: Casey Eickhoff <[email protected]>
1 parent 99c33a9 commit c1669d2

File tree

5 files changed

+122
-28
lines changed

5 files changed

+122
-28
lines changed

.changeset/rich-bears-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@spectrum-web-components/action-button': minor
3+
---
4+
5+
- **Fixed** : Action buttons with href attributes now properly detects modifier keys and skips the proxy click, allowing only native browser behavior to proceed.

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ parameters:
1414
# 3. Commit this change to the PR branch where the changes exist.
1515
current_golden_images_hash:
1616
type: string
17-
default: a6fdd13f78f7931bbf15af5aef3c79f0423df3b5
17+
default: c5b517432c05e1eeac576ad651233cef83957ce9
1818
wireit_cache_name:
1919
type: string
2020
default: wireit

packages/action-button/stories/action-button.stories.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,12 @@ href.args = {
4747
href: 'https://github.com/adobe/spectrum-web-components',
4848
icon: `<sp-icon-edit hidden slot="icon"></sp-icon-edit>`,
4949
};
50+
51+
export const hrefWithTarget = (): TemplateResult => html`
52+
<sp-action-button
53+
href="https://github.com/adobe/spectrum-web-components"
54+
target="_blank"
55+
>
56+
Click me
57+
</sp-action-button>
58+
`;

packages/button/src/ButtonBase.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
9393
return false;
9494
}
9595

96-
if (this.shouldProxyClick()) {
96+
if (this.shouldProxyClick(event as MouseEvent)) {
9797
return;
9898
}
9999
}
@@ -102,10 +102,19 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
102102
this.focus();
103103
}
104104

105-
private shouldProxyClick(): boolean {
105+
private shouldProxyClick(event?: MouseEvent): boolean {
106106
let handled = false;
107+
108+
// Don't proxy clicks with modifier keys (Command/Meta, Ctrl, Shift, Alt)
109+
if (
110+
event &&
111+
(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
112+
) {
113+
return false;
114+
}
115+
107116
if (this.anchorElement) {
108-
// click HTML anchor element by proxy
117+
// Click HTML anchor element by proxy, but only for non-modified clicks
109118
this.anchorElement.click();
110119
handled = true;
111120
// if the button type is `submit` or `reset`
@@ -127,7 +136,8 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
127136
${super.renderAnchor({
128137
id: 'button',
129138
ariaHidden: true,
130-
className: 'button anchor hidden',
139+
className: 'button anchor',
140+
tabindex: -1,
131141
})}
132142
`;
133143
}
@@ -234,8 +244,16 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
234244
}
235245

236246
if (this.anchorElement) {
237-
this.anchorElement.addEventListener('focus', this.proxyFocus);
247+
// Ensure the anchor element is not focusable directly via tab
238248
this.anchorElement.tabIndex = -1;
249+
250+
// Make sure it has proper ARIA attributes
251+
if (!this.anchorElement.hasAttribute('aria-hidden')) {
252+
this.anchorElement.setAttribute('aria-hidden', 'true');
253+
}
254+
255+
// Set up focus delegation
256+
this.anchorElement.addEventListener('focus', this.proxyFocus);
239257
}
240258
}
241259
protected override update(changes: PropertyValues): void {

packages/button/test/button.test.ts

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -719,28 +719,6 @@ describe('Button', () => {
719719
expect(submitSpy.callCount).to.equal(1);
720720
expect(resetSpy.callCount).to.equal(1);
721721
});
722-
it('proxies click by [href]', async () => {
723-
const clickSpy = spy();
724-
const el = await fixture<Button>(html`
725-
<sp-button href="test_url">With Href</sp-button>
726-
`);
727-
728-
await elementUpdated(el);
729-
(
730-
el as unknown as {
731-
anchorElement: HTMLAnchorElement;
732-
}
733-
).anchorElement.addEventListener('click', (event: Event): void => {
734-
event.preventDefault();
735-
event.stopPropagation();
736-
clickSpy();
737-
});
738-
expect(clickSpy.callCount).to.equal(0);
739-
740-
el.click();
741-
await elementUpdated(el);
742-
expect(clickSpy.callCount).to.equal(1);
743-
});
744722
it('manages "active" while focused', async () => {
745723
const el = await fixture<Button>(html`
746724
<sp-button label="Button">
@@ -849,5 +827,89 @@ describe('Button', () => {
849827
expect(el.hasAttribute('static-color')).to.be.false;
850828
});
851829
});
830+
it('handles modifier key clicks correctly', async () => {
831+
const el = await fixture<Button>(html`
832+
<sp-button href="#test">Button with href</sp-button>
833+
`);
834+
835+
await elementUpdated(el);
836+
837+
const anchorElement = el.shadowRoot?.querySelector(
838+
'.anchor'
839+
) as HTMLAnchorElement;
840+
expect(anchorElement).to.not.be.undefined;
841+
842+
// Set up spies instead of counters
843+
const buttonClickSpy = spy();
844+
const anchorClickSpy = spy();
845+
846+
// Prevent actual navigation but track anchor clicks with spy
847+
anchorElement.addEventListener('click', (event) => {
848+
event.preventDefault();
849+
anchorClickSpy();
850+
});
851+
852+
// Track button clicks with spy
853+
el.addEventListener('click', buttonClickSpy);
854+
855+
// Test normal click - should proxy to anchor
856+
const normalClick = new MouseEvent('click', {
857+
bubbles: true,
858+
cancelable: true,
859+
});
860+
861+
buttonClickSpy.resetHistory();
862+
anchorClickSpy.resetHistory();
863+
864+
el.dispatchEvent(normalClick);
865+
await elementUpdated(el);
866+
867+
expect(
868+
anchorClickSpy.called,
869+
'Normal click should be proxied to the anchor'
870+
).to.be.true;
871+
872+
buttonClickSpy.resetHistory();
873+
anchorClickSpy.resetHistory();
874+
875+
const metaClick = new MouseEvent('click', {
876+
bubbles: true,
877+
cancelable: true,
878+
metaKey: true,
879+
});
880+
881+
el.dispatchEvent(metaClick);
882+
await elementUpdated(el);
883+
884+
expect(
885+
buttonClickSpy.called,
886+
'Meta+click should be received by the button'
887+
).to.be.true;
888+
expect(
889+
anchorClickSpy.called,
890+
'Meta+click should NOT be proxied to the anchor'
891+
).to.be.false;
892+
893+
buttonClickSpy.resetHistory();
894+
anchorClickSpy.resetHistory();
895+
896+
const ctrlClick = new MouseEvent('click', {
897+
bubbles: true,
898+
cancelable: true,
899+
ctrlKey: true,
900+
});
901+
902+
el.dispatchEvent(ctrlClick);
903+
await elementUpdated(el);
904+
905+
expect(
906+
buttonClickSpy.called,
907+
'Ctrl+click should be received by the button'
908+
).to.be.true;
909+
expect(
910+
anchorClickSpy.called,
911+
'Ctrl+click should NOT be proxied to the anchor'
912+
).to.be.false;
913+
});
852914
});
853915
});

0 commit comments

Comments
 (0)