Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(button): adds pending button, fixes #3162 #3163

Merged
merged 36 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
305c7e6
feat(button): adds pending button, fixes #3162
benjamind Apr 28, 2023
52417fe
chore(button): fixed merge conflicts
TarunAdobe Dec 4, 2023
470b0f5
chore(button): import sp-progress-circle whenever required
TarunAdobe Dec 4, 2023
56e822c
chore: updated golden image hash
TarunAdobe Dec 4, 2023
dfc98fe
chore(button): updated aria-label on changing pending state
TarunAdobe Dec 4, 2023
0be1ae7
chore(button): removed redudant styles and functions
TarunAdobe Dec 5, 2023
b31e47a
chore(button): added tshirt sizing to the progress-circle
TarunAdobe Dec 7, 2023
49d9a8d
Merge branch 'main' into delarre/pending-button
TarunAdobe Dec 7, 2023
c34de46
Merge branch 'main' into delarre/pending-button
TarunAdobe Dec 11, 2023
312b676
chore(button): removed isPending state
TarunAdobe Dec 12, 2023
9353e4f
Merge branch 'main' into delarre/pending-button
TarunAdobe Dec 12, 2023
e305d44
chore(button): update aria-label on pending toggle
TarunAdobe Dec 12, 2023
fe59c9b
chore: fixed merge conflict
TarunAdobe Dec 13, 2023
f327507
chore: updated golden image hash
TarunAdobe Dec 13, 2023
3233d75
chore(button): removed ispending and timeout
TarunAdobe Dec 15, 2023
6acf088
chore: merged main and fixed conflict
TarunAdobe Dec 15, 2023
80f4baf
chore(button): updated css animation names
TarunAdobe Dec 18, 2023
e1650cb
Merge branch 'main' into delarre/pending-button
TarunAdobe Dec 18, 2023
1235028
chore(button): update aria-label on removing pending
TarunAdobe Dec 18, 2023
c1b54b5
chore: fixed merge conflicts
TarunAdobe Dec 19, 2023
3201c55
chore(button): added docs for pending state
TarunAdobe Dec 19, 2023
3c6eaeb
chore: updated golden image hash
TarunAdobe Dec 19, 2023
cdad70d
chore(button): update arialabel on disabled change in pending state
TarunAdobe Dec 20, 2023
4b3f03f
Merge branch 'main' into delarre/pending-button
TarunAdobe Dec 20, 2023
a96ae36
chore: updated golden image hash
TarunAdobe Dec 21, 2023
f67a9a5
fix(button): updated aria-label management in button
TarunAdobe Jan 3, 2024
db88621
fix(button): fixed aria-label management and added more tests
TarunAdobe Jan 3, 2024
740fb8c
chore: merged main
TarunAdobe Jan 3, 2024
b5e3523
fix(button): revert back the aria-label management logic in ButtonBase
TarunAdobe Jan 3, 2024
07e63b9
chore: updated golden image hash
TarunAdobe Jan 3, 2024
0258c3a
fix(button): shifted aria-label management to updated in button
TarunAdobe Jan 3, 2024
4c99a45
chore: merged main
TarunAdobe Jan 4, 2024
e8720ac
chore: udpated golden image hash
TarunAdobe Jan 4, 2024
d65480f
chore: updated golden image hash
TarunAdobe Jan 4, 2024
f36e8ad
chore(button): improved code readability
TarunAdobe Jan 5, 2024
3ffb308
Merge branch 'main' into delarre/pending-button
TarunAdobe Jan 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ executors:
parameters:
current_golden_images_hash:
type: string
default: d8be48bd925210181c3ec6933abbc3aade090bcb
default: 470b0f5294f4bffae31534b5734ecfd6317316ec
wireit_cache_name:
type: string
default: wireit
Expand Down
78 changes: 77 additions & 1 deletion packages/button/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ governing permissions and limitations under the License.

import {
CSSResultArray,
html,
PropertyValues,
SizedMixin,
TemplateResult,
} from '@spectrum-web-components/base';
import { property } from '@spectrum-web-components/base/src/decorators.js';
import { ButtonBase } from './ButtonBase.js';
import buttonStyles from './button.css.js';
import { when } from '@spectrum-web-components/base/src/directives.js';

export type DeprecatedButtonVariants = 'cta' | 'overBackground';
export type ButtonStatics = 'white' | 'black';
Expand Down Expand Up @@ -51,6 +54,42 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
return [...super.styles, buttonStyles];
}

protected pendingCooldown = -1;

constructor() {
super();
this.addEventListener(
'click',
(event: Event): void | boolean => {
if (this.pending) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
return false;
}
},
{
capture: true,
}
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts on this versus using pointer-events: none on the attribute selector?

If we use the event listener, I think we should only add it when pending rather than always have the listener attached.

Copy link
Contributor

Choose a reason for hiding this comment

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

pointer-events:none should be better in this case so as to avoid any event propagation at all during pending state.
Note: we are overriding the click function to avoid clicking on pending either way so even with pointer-events:none we won't run into cases where users tries to click the button programatically while in pending state!

}

@property({ type: Boolean, reflect: true, attribute: 'is-pending' })
public isPending = false;

@property({ type: String, attribute: 'pending-label' })
public pendingLabel = 'Pending';

@property({ type: Boolean, reflect: true, attribute: true })
public pending = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really understand the use of pending and isPending. Can you add some comments to clarify the workflow that's being acheived?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah pending is the indication that this is a pending state button, while is-pending is added/removed during the pending state. Agree needs comments.

Copy link
Contributor

Choose a reason for hiding this comment

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

That timing seems weird, though it does make the code make so much more sense.

Based on this documentation, it feels like pending should be an immediate state. Was there something specific to your application requirements that lead to this relationship?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well it was more a matter of convenience at point of use. If the pending state is added as an indicator of future behavior, then the click handler behaves like a normal button, with the click event firing on first click. The button then adds its own isPending state to track its timeout period and disable further clicks during that time.

If the consumer has to add the pending state when click occurs, thats a little more work for the consumer to deal with? Not sure, I could go both ways on this, but it felt convenient to have it this way, and it marks the button in the consumers render template as a pending state button so its quite clear to see that the behavior will be distinct here, rather than a side effect of the buttons click handler?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is an interesting approach, but not seemingly what the code says?

        if (changedProperties.has('pending')) {
            if (this.pending) {
                this.pendingCooldown = window.setTimeout(() => {
                    if (!this.disabled) {
                        this.isPending = true;
                        this.setAttribute('aria-label', this.pendingLabel);
                    }
                }, 1000);
            } else if (this.pending === false && this.isPending) {
                window.clearTimeout(this.pendingCooldown);
                this.isPending = false;
                this.setAttribute('aria-label', this.label || '');
            }
        }

This may be updated functionality, but this says that as soon as you set pending you start the clock for isPending.

I discussed why this might be in with the design team, and they outlined a seemingly valid use case of no wanting to go to "visually pending" and then "processed" in flicker speed progression. It's valid, but it's valid at any point in the process where you might switch to pending, which makes me wonder if it. would be better to require an application manage any staged loading they do local to where there can be some understanding of the time required to do the actual task in question.

I can be convinced of either way, with clear comments that outline a workflow that we all see the same way, but right now I'm leaning towards it being better to only offer pending and it being immediate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes sorry forgot that nuance (its been a while since i started this one)....

I think if we leave management of the visually pending state to the consumer, like as not it will get implemented poorly in many places and result in a multitude of flickering bugs in a variety of places with solutions to it varying across the board.

But yes the state flow as I recall now is:

  • Enabled button
  • Click
  • Click handler - consumer assigns pending state
  • isPending timeout begins
  • isPending && pending causes display of 'visually pending' state
  • Time goes by
  • Consumer removes pending state
  • isPending is removed
  • Enabled button.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this is ugly pretty much regardless... @TarunAdobe can you test this approach which would simplify to a single state variable.

  • Enable button
  • Click
  • Consumer assignes pending
  • The 'visually pending' state is applied via an immediate transition or animation that has a delay of 1000ms but is customizable via a CSS Custom Property
  • Time goes by
  • Consumer removes pending
  • The 'visually pending' state is removed via an immediate transition or animation that has no delay
  • Button is enable

Copy link
Contributor

Choose a reason for hiding this comment

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

done


public override click(): void {
if (this.isPending) {
return;
}
super.click();
}

/**
* The visual variant to apply to this button.
*/
Expand Down Expand Up @@ -127,11 +166,30 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
public set quiet(quiet: boolean) {
this.treatment = quiet ? 'outline' : 'fill';
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add back this line.

Copy link
Contributor

Choose a reason for hiding this comment

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

done hehe

public get quiet(): boolean {
return this.treatment === 'outline';
}

protected override willUpdate(
changedProperties: PropertyValues<this>
): void {
super.willUpdate(changedProperties);
if (changedProperties.has('pending')) {
if (this.pending) {
this.pendingCooldown = window.setTimeout(() => {
if (!this.disabled) {
this.isPending = true;
this.setAttribute('aria-label', this.pendingLabel);
}
}, 1000);
} else if (this.pending === false && this.isPending) {
window.clearTimeout(this.pendingCooldown);
this.isPending = false;
this.setAttribute('aria-label', this.label || '');
}
}
}

protected override firstUpdated(changes: PropertyValues<this>): void {
super.firstUpdated(changes);
// There is no Spectrum design context for an `<sp-button>` without a variant
Expand All @@ -140,4 +198,22 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
this.setAttribute('variant', this.variant);
}
}

protected override renderButton(): TemplateResult {
return html`
${this.buttonContent}
${when(this.isPending, () => {
import(
'@spectrum-web-components/progress-circle/sp-progress-circle.js'
);
return html`
<sp-progress-circle
indeterminate
static="white"
size="s"
Copy link
Contributor

Choose a reason for hiding this comment

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

Is size="s" the right size for all Button sizes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In internal discussion with the Spectrum-CSS team, specifically @KayiuCarlson we concluded that there are no good t-shirt sizes right now to use for the progress circle inside of a button in these cases. So we concluded that we should fudge the sizes manually here to match the workflow-icon sizes.

Copy link
Contributor

Choose a reason for hiding this comment

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

now using tshirt sizing for progress-circle.

></sp-progress-circle>
`;
})}
`;
}
}
29 changes: 29 additions & 0 deletions packages/button/src/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,32 @@ governing permissions and limitations under the License.
border-color: highlight;
}
}

::slotted([slot='icon']) {
inset: unset;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this for?

Copy link
Contributor

Choose a reason for hiding this comment

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

same for this one... It doesn't seem to do anything in context of what we are trying to achieve.


#container {
position: relative;
display: flex;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This element no longer seems to exist?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think so... maybe it did during the initial phase of this PR however now it's useless. so im removing it.


sp-progress-circle {
display: none;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any time when not pending that this would be rendered to the DOM so as to need this rule?

Copy link
Contributor

Choose a reason for hiding this comment

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

checked and found this is redundant!

Copy link
Contributor

Choose a reason for hiding this comment

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

With the removal of isPending variable in the button. this is now back and useful again as we want to show the progress-circle only when the is-pending attribute is set after the animation


:host([pending][is-pending]) {
cursor: default;
}

:host([pending][is-pending]) sp-progress-circle {
display: block;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
Comment on lines +81 to +82
Copy link
Contributor

Choose a reason for hiding this comment

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

For reference: left here is technically correct, in that translate() doesn't really support logical properties, but CSS attempts not to use it when possible.

}

:host([pending][is-pending]) slot[name='icon'],
:host([pending][is-pending]) #label {
visibility: hidden;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't remember what visibly hiding this content does to the accessibility tree right off, so making a note to confirm.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checked in voiceover and the label seems to be hidden? Is there some other way I can test?

}
3 changes: 2 additions & 1 deletion packages/button/src/spectrum-button.css
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,8 @@ governing permissions and limitations under the License.
)
);
}
:host([disabled]) {
:host([disabled]),
:host([is-pending]) {
background-color: var(
--highcontrast-button-background-color-disabled,
var(
Expand Down
7 changes: 6 additions & 1 deletion packages/button/src/spectrum-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ const config = {
converter.classToHost(),
converter.classToAttribute('spectrum-Button--quiet'),
converter.classToAttribute('spectrum-Button--emphasized'),
converter.classToAttribute('is-disabled', 'disabled'),
converter.classToAttribute('is-selected', 'selected'),
converter.classToAttribute('is-focused', 'focused'),
/**
* HACK!
* This relies on the fact that spectrum-css is using both `&:disabled` and `&.is-disabled` in the selectors
* for disabled states. We're using the class based selector here to also emit a `is-pending` selector.
*/
converter.classToAttribute('is-disabled', 'is-pending'),
converter.pseudoToAttribute('disabled', 'disabled'),
converter.pseudoToAttribute('active', 'active'),
converter.classToAttribute(
Expand Down
50 changes: 50 additions & 0 deletions packages/button/stories/button-accent-fill-pending.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { TemplateResult } from '@spectrum-web-components/base';
import { Properties, renderButtonSet } from './index.js';
import { args, argTypes } from './index.js';

const variant = 'accent';
const treatment = 'fill';
const pending = true;

export default {
component: 'sp-button',
title: 'Button/Accent/Fill/Pending',
args: {
...args,
variant,
treatment,
pending,
},
argTypes,
};

export const s = (args: Properties): TemplateResult => renderButtonSet(args);
s.args = {
size: 's',
};

export const m = (args: Properties): TemplateResult => renderButtonSet(args);
m.args = {
size: 'm',
};

export const l = (args: Properties): TemplateResult => renderButtonSet(args);
l.args = {
size: 'l',
};

export const XL = (args: Properties): TemplateResult => renderButtonSet(args);
XL.args = {
size: 'xl',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { TemplateResult } from '@spectrum-web-components/base';
import { Properties, renderButtonSet } from './index.js';
import { args, argTypes } from './index.js';

const variant = 'accent';
const treatment = 'outline';
const pending = true;

export default {
component: 'sp-button',
title: 'Button/Accent/Outline/Pending',
args: {
...args,
variant,
treatment,
pending,
},
argTypes,
};

export const s = (args: Properties): TemplateResult => renderButtonSet(args);
s.args = {
size: 's',
};

export const m = (args: Properties): TemplateResult => renderButtonSet(args);
m.args = {
size: 'm',
};

export const l = (args: Properties): TemplateResult => renderButtonSet(args);
l.args = {
size: 'l',
};

export const XL = (args: Properties): TemplateResult => renderButtonSet(args);
XL.args = {
size: 'xl',
};
50 changes: 50 additions & 0 deletions packages/button/stories/button-black-fill-pending.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { TemplateResult } from '@spectrum-web-components/base';
import { Properties, renderButtonSet } from './index.js';
import { args, argTypes } from './index.js';

const variant = 'black';
const treatment = 'fill';
const pending = true;

export default {
component: 'sp-button',
title: 'Button/Black/Fill/Pending',
args: {
...args,
variant,
treatment,
pending,
},
argTypes,
};

export const s = (args: Properties): TemplateResult => renderButtonSet(args);
s.args = {
size: 's',
};

export const m = (args: Properties): TemplateResult => renderButtonSet(args);
m.args = {
size: 'm',
};

export const l = (args: Properties): TemplateResult => renderButtonSet(args);
l.args = {
size: 'l',
};

export const XL = (args: Properties): TemplateResult => renderButtonSet(args);
XL.args = {
size: 'xl',
};
50 changes: 50 additions & 0 deletions packages/button/stories/button-black-outline-pending.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2020 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
import { TemplateResult } from '@spectrum-web-components/base';
import { Properties, renderButtonSet } from './index.js';
import { args, argTypes } from './index.js';

const variant = 'black';
const treatment = 'outline';
const pending = true;

export default {
component: 'sp-button',
title: 'Button/Black/Outline/Pending',
args: {
...args,
variant,
treatment,
pending,
},
argTypes,
};

export const s = (args: Properties): TemplateResult => renderButtonSet(args);
s.args = {
size: 's',
};

export const m = (args: Properties): TemplateResult => renderButtonSet(args);
m.args = {
size: 'm',
};

export const l = (args: Properties): TemplateResult => renderButtonSet(args);
l.args = {
size: 'l',
};

export const XL = (args: Properties): TemplateResult => renderButtonSet(args);
XL.args = {
size: 'xl',
};
Loading
Loading