Skip to content

Commit 49e94bc

Browse files
committed
feat(button): use synthetic button instead of native
1 parent 134bafc commit 49e94bc

27 files changed

+897
-418
lines changed

packages/action-group/src/action-group.css

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,6 @@ governing permissions and limitations under the License.
1212

1313
@import './spectrum-action-group.css';
1414

15-
/**
16-
* The following styles translate the Spectrum CSS usage of `border-radius` directly on `.spectrum-ActionGroup-item`
17-
* to a usage of the `--spectrum-actionbutton-border-radius` custom property so that it pieces the shadow DOM in
18-
* `<sp-action-button>` appropriately.
19-
**/
20-
21-
:host([compact]:not([quiet])) ::slotted(*) {
22-
/* .spectrum-ActionGroup--compact:not(.spectrum-ActionGroup--quiet) .spectrum-ActionGroup-item */
23-
--spectrum-actionbutton-border-radius: 0;
24-
}
25-
26-
:host([compact][vertical]:not([quiet])) ::slotted(*) {
27-
/* .spectrum-ActionGroup--compact:not(.spectrum-ActionGroup--quiet).spectrum-ActionGroup--vertical .spectrum-ActionGroup-item */
28-
--spectrum-actionbutton-border-radius: 0;
29-
}
30-
31-
:host([dir='ltr'][compact]:not([quiet])) ::slotted(*:first-child),
32-
:host([dir='rtl'][compact]:not([quiet])) ::slotted(*:last-child) {
33-
--spectrum-actionbutton-border-radius: var(
34-
--spectrum-alias-border-radius-regular
35-
)
36-
0 0 var(--spectrum-alias-border-radius-regular);
37-
}
38-
39-
:host([dir='rtl'][compact]:not([quiet])) ::slotted(*:first-child),
40-
:host([dir='ltr'][compact]:not([quiet])) ::slotted(*:last-child) {
41-
--spectrum-actionbutton-border-radius: 0
42-
var(--spectrum-alias-border-radius-regular)
43-
var(--spectrum-alias-border-radius-regular) 0;
44-
}
45-
46-
:host([compact][vertical]:not([quiet])) ::slotted(*:first-child) {
47-
--spectrum-actionbutton-border-radius: var(
48-
--spectrum-alias-border-radius-regular
49-
)
50-
var(--spectrum-alias-border-radius-regular) 0 0;
51-
}
52-
53-
:host([compact][vertical]:not([quiet])) ::slotted(*:last-child) {
54-
--spectrum-actionbutton-border-radius: 0 0
55-
var(--spectrum-alias-border-radius-regular)
56-
var(--spectrum-alias-border-radius-regular);
57-
}
58-
5915
/**
6016
* The following styles corrects issues found in the Spectrum CSS output
6117
* that are tracked via: https://github.com/adobe/spectrum-css/issues/795

packages/action-menu/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"license": "Apache-2.0",
5151
"dependencies": {
5252
"@spectrum-web-components/base": "^0.1.3",
53+
"@spectrum-web-components/button": "^0.9.4",
5354
"@spectrum-web-components/dropdown": "^0.8.5",
5455
"@spectrum-web-components/icon": "^0.6.3",
5556
"@spectrum-web-components/icons-workflow": "^0.3.6",

packages/action-menu/src/ActionMenu.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ governing permissions and limitations under the License.
1313
import {
1414
CSSResultArray,
1515
TemplateResult,
16-
property,
1716
PropertyValues,
1817
html,
18+
ifDefined,
1919
} from '@spectrum-web-components/base';
2020
import { DropdownBase } from '@spectrum-web-components/dropdown';
21+
import '@spectrum-web-components/button/sp-action-button.js';
2122
import { ObserveSlotText } from '@spectrum-web-components/shared/src/observe-slot-text.js';
2223
import { MoreIcon } from '@spectrum-web-components/icons-workflow';
2324
import actionMenuStyles from './action-menu.css.js';
@@ -30,12 +31,6 @@ export class ActionMenu extends ObserveSlotText(DropdownBase, 'label') {
3031
return [...super.styles, actionMenuStyles];
3132
}
3233

33-
@property({ type: Boolean, reflect: true })
34-
public selected = false;
35-
36-
@property({ type: Boolean, reflect: true })
37-
public quiet = true;
38-
3934
protected listRole = 'menu';
4035
protected itemRole = 'menuitem';
4136
private get hasLabel(): boolean {
@@ -45,32 +40,42 @@ export class ActionMenu extends ObserveSlotText(DropdownBase, 'label') {
4540
protected get buttonContent(): TemplateResult[] {
4641
return [
4742
html`
48-
<slot name="icon">
43+
<slot name="icon" slot="icon">
4944
<sp-icon size="m" class="icon">
5045
${MoreIcon({ hidden: this.hasLabel })}
5146
</sp-icon>
5247
</slot>
53-
<div id="label" ?hidden=${!this.hasLabel}>
54-
<slot
55-
name="label"
56-
id="slot"
57-
@slotchange=${this.manageTextObservedSlot}
58-
></slot>
59-
</div>
48+
<slot name="label"></slot>
6049
`,
6150
];
6251
}
6352

53+
protected get renderButton(): TemplateResult {
54+
return html`
55+
<sp-action-button
56+
quiet
57+
?selected=${this.open}
58+
aria-haspopup="true"
59+
aria-controls="popover"
60+
aria-expanded=${this.open ? 'true' : 'false'}
61+
aria-label=${ifDefined(this.label || undefined)}
62+
id="button"
63+
class="button"
64+
@blur=${this.onButtonBlur}
65+
@click=${this.onButtonClick}
66+
@focus=${this.onButtonFocus}
67+
?disabled=${this.disabled}
68+
>
69+
${this.buttonContent}
70+
</sp-action-button>
71+
`;
72+
}
73+
6474
protected updated(changedProperties: PropertyValues): void {
6575
super.updated(changedProperties);
66-
if (changedProperties.has('open')) {
67-
this.selected = this.open;
68-
}
69-
if (changedProperties.has('quiet')) {
70-
this.quiet = true;
71-
}
7276
if (changedProperties.has('invalid')) {
7377
this.invalid = false;
7478
}
79+
this.quiet = true;
7580
}
7681
}

packages/action-menu/src/action-menu.css

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,16 @@ OF ANY KIND, either express or implied. See the License for the specific languag
1010
governing permissions and limitations under the License.
1111
*/
1212

13-
:host {
13+
:host,
14+
.button {
1415
min-width: 0;
16+
width: auto;
1517
}
1618

1719
:host([quiet]) {
1820
min-width: 0;
1921
}
2022

21-
#button {
22-
width: auto;
23-
}
24-
2523
::slotted([slot='icon']) {
2624
flex-shrink: 0;
2725
}

packages/button/sp-field-button.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
Copyright 2020 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
import { FieldButton } from './src/FieldButton.js';
13+
14+
customElements.define('sp-field-button', FieldButton);
15+
16+
declare global {
17+
interface HTMLElementTagNameMap {
18+
'sp-field-button': FieldButton;
19+
}
20+
}

packages/button/src/ButtonBase.ts

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
TemplateResult,
1717
CSSResultArray,
1818
query,
19-
ifDefined,
19+
PropertyValues,
2020
} from '@spectrum-web-components/base';
2121
import { LikeAnchor } from '@spectrum-web-components/shared/src/like-anchor.js';
2222
import { Focusable } from '@spectrum-web-components/shared/src/focusable.js';
@@ -37,18 +37,24 @@ export class ButtonBase extends LikeAnchor(
3737
return this.slotContentIsPresent;
3838
}
3939

40+
@property({ type: Boolean, reflect: true })
41+
public active = false;
42+
43+
@property({ type: String })
44+
public type: 'button' | 'submit' | 'reset' = 'button';
45+
4046
@property({ type: Boolean, reflect: true, attribute: 'icon-right' })
4147
protected iconRight = false;
4248

43-
private get hasLabel(): boolean {
49+
protected get hasLabel(): boolean {
4450
return this.slotHasContent;
4551
}
4652

4753
@query('.button')
4854
private buttonElement!: HTMLButtonElement;
4955

5056
public get focusElement(): HTMLElement {
51-
return this.buttonElement;
57+
return this.buttonElement || this;
5258
}
5359

5460
protected get buttonContent(): TemplateResult[] {
@@ -72,15 +78,33 @@ export class ButtonBase extends LikeAnchor(
7278
return content;
7379
}
7480

81+
public click(): void {
82+
if (this.disabled) {
83+
return;
84+
}
85+
86+
if (this.shouldProxyClick()) {
87+
return;
88+
}
89+
90+
super.click();
91+
}
92+
93+
private shouldProxyClick(): boolean {
94+
if (this.type !== 'button') {
95+
const proxy = document.createElement('button');
96+
proxy.type = this.type;
97+
this.insertAdjacentElement('afterend', proxy);
98+
proxy.click();
99+
proxy.remove();
100+
return true;
101+
}
102+
return false;
103+
}
104+
75105
protected renderButton(): TemplateResult {
76106
return html`
77-
<button
78-
id="button"
79-
class="button"
80-
aria-label=${ifDefined(this.label)}
81-
>
82-
${this.buttonContent}
83-
</button>
107+
${this.buttonContent}
84108
`;
85109
}
86110

@@ -93,4 +117,82 @@ export class ButtonBase extends LikeAnchor(
93117
})
94118
: this.renderButton();
95119
}
120+
121+
private handleKeydown(event: KeyboardEvent): void {
122+
const { code } = event;
123+
switch (code) {
124+
case 'Space':
125+
this.addEventListener('keyup', this.handleKeyup);
126+
this.active = true;
127+
break;
128+
default:
129+
break;
130+
}
131+
}
132+
133+
private handleKeypress(event: KeyboardEvent): void {
134+
const { code } = event;
135+
switch (code) {
136+
case 'Enter':
137+
this.click();
138+
break;
139+
default:
140+
break;
141+
}
142+
}
143+
144+
private handleKeyup(event: KeyboardEvent): void {
145+
const { code } = event;
146+
switch (code) {
147+
case 'Space':
148+
this.removeEventListener('keyup', this.handleKeyup);
149+
this.active = false;
150+
this.click();
151+
break;
152+
default:
153+
break;
154+
}
155+
}
156+
157+
private handleFocusout(): void {
158+
this.active = false;
159+
}
160+
161+
private manageRole(): void {
162+
if (this.href && this.href.length > 0) {
163+
this.removeAttribute('role');
164+
this.removeEventListener('keydown', this.handleKeydown);
165+
this.removeEventListener('keypress', this.handleKeypress);
166+
} else if (!this.hasAttribute('role')) {
167+
this.setAttribute('role', 'button');
168+
this.addEventListener('keydown', this.handleKeydown);
169+
this.addEventListener('keypress', this.handleKeypress);
170+
}
171+
}
172+
173+
protected firstUpdated(changed: PropertyValues): void {
174+
super.firstUpdated(changed);
175+
if (!this.hasAttribute('tabindex')) {
176+
this.tabIndex = 0;
177+
}
178+
this.manageRole();
179+
this.addEventListener('click', this.shouldProxyClick);
180+
}
181+
182+
protected updated(changed: PropertyValues): void {
183+
super.updated(changed);
184+
if (changed.has('href')) {
185+
this.manageRole();
186+
}
187+
if (changed.has('label')) {
188+
this.setAttribute('aria-label', this.label || '');
189+
}
190+
if (changed.has('active')) {
191+
if (this.active) {
192+
this.addEventListener('focusout', this.handleFocusout);
193+
} else {
194+
this.removeEventListener('focusout', this.handleFocusout);
195+
}
196+
}
197+
}
96198
}

packages/button/src/FieldButton.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright 2020 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
13+
import {
14+
html,
15+
CSSResultArray,
16+
TemplateResult,
17+
} from '@spectrum-web-components/base';
18+
import { ButtonBase } from './ButtonBase.js';
19+
import buttonStyles from './field-button.css.js';
20+
21+
/**
22+
* A Spectrum button control.
23+
* @element sp-field-button
24+
*/
25+
export class FieldButton extends ButtonBase {
26+
public static get styles(): CSSResultArray {
27+
return [...super.styles, buttonStyles];
28+
}
29+
30+
protected get buttonContent(): TemplateResult[] {
31+
const icon = html`
32+
<slot name="icon"></slot>
33+
`;
34+
const content = [
35+
html`
36+
<slot
37+
?hidden=${!this.hasLabel}
38+
id="slot"
39+
@slotchange=${this.manageTextObservedSlot}
40+
></slot>
41+
`,
42+
];
43+
if (!this.hasIcon) {
44+
return content;
45+
}
46+
this.iconRight ? content.push(icon) : content.unshift(icon);
47+
return content;
48+
}
49+
}

0 commit comments

Comments
 (0)