Skip to content

Commit

Permalink
feat(reactive-controller): new pending state controller (#4605)
Browse files Browse the repository at this point in the history
* feat: added a pending state controller
---------

Co-authored-by: TaraT <ttomar@adobe.com>
Co-authored-by: Rajdeep Chandra <rajrock38@gmail.com>
Co-authored-by: Rúben Carvalho <rubcar@sapo.pt>
  • Loading branch information
4 people authored Sep 17, 2024
1 parent 685d764 commit 68baf94
Show file tree
Hide file tree
Showing 9 changed files with 404 additions and 73 deletions.
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: 1bee3571a815481151a0d4fad5a225cb0e8d36a1
default: 05cb901762d5af33e21e113ed598cecea3488def
wireit_cache_name:
type: string
default: wireit
Expand Down
67 changes: 19 additions & 48 deletions packages/button/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
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';
import { PendingStateController } from '@spectrum-web-components/reactive-controllers/src/PendingState.js';

export type DeprecatedButtonVariants = 'cta' | 'overBackground';
export type ButtonStatics = 'white' | 'black';
Expand Down Expand Up @@ -61,7 +61,16 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
@property({ type: Boolean, reflect: true, attribute: true })
public pending = false;

private cachedAriaLabel: string | null = null;
public pendingStateController: PendingStateController<this>;

/**
* Initializes the `PendingStateController` for the Button component.
* The `PendingStateController` manages the pending state of the Button.
*/
constructor() {
super();
this.pendingStateController = new PendingStateController(this);
}

public override click(): void {
if (this.pending) {
Expand Down Expand Up @@ -158,61 +167,23 @@ export class Button extends SizedMixin(ButtonBase, { noDefaultSize: true }) {
if (!this.hasAttribute('variant')) {
this.setAttribute('variant', this.variant);
}
if (this.pending) {
this.pendingStateController.hostUpdated();
}
}

protected override update(changes: PropertyValues): void {
super.update(changes);
}

protected override updated(changed: PropertyValues): void {
super.updated(changed);

if (changed.has('pending')) {
if (
this.pending &&
this.pendingLabel !== this.getAttribute('aria-label')
) {
if (!this.disabled) {
this.cachedAriaLabel =
this.getAttribute('aria-label') || '';
this.setAttribute('aria-label', this.pendingLabel);
}
} else if (!this.pending && this.cachedAriaLabel) {
this.setAttribute('aria-label', this.cachedAriaLabel);
} else if (!this.pending && this.cachedAriaLabel === '') {
this.removeAttribute('aria-label');
}
}

if (changed.has('disabled')) {
if (
!this.disabled &&
this.pendingLabel !== this.getAttribute('aria-label')
) {
if (this.pending) {
this.cachedAriaLabel =
this.getAttribute('aria-label') || '';
this.setAttribute('aria-label', this.pendingLabel);
}
} else if (this.disabled && this.cachedAriaLabel) {
this.setAttribute('aria-label', this.cachedAriaLabel);
} else if (this.disabled && this.cachedAriaLabel == '') {
this.removeAttribute('aria-label');
}
}
}

protected override renderButton(): TemplateResult {
return html`
${this.buttonContent}
${when(this.pending, () => {
import(
'@spectrum-web-components/progress-circle/sp-progress-circle.js'
);
return html`
<sp-progress-circle
indeterminate
static="white"
aria-hidden="true"
></sp-progress-circle>
`;
})}
${this.pendingStateController.renderPendingState()}
`;
}
}
21 changes: 18 additions & 3 deletions packages/button/src/ButtonBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
if (!this.hasAttribute('tabindex')) {
this.setAttribute('tabindex', '0');
}
if (changed.has('label')) {
if (this.label) {
this.setAttribute('aria-label', this.label);
} else {
this.removeAttribute('aria-label');
}
}
this.manageAnchor();
this.addEventListener('keydown', this.handleKeydown);
this.addEventListener('keypress', this.handleKeypress);
Expand All @@ -215,12 +222,20 @@ export class ButtonBase extends ObserveSlotText(LikeAnchor(Focusable), '', [
if (changed.has('href')) {
this.manageAnchor();
}
if (changed.has('label')) {
this.setAttribute('aria-label', this.label || '');
}

if (this.anchorElement) {
this.anchorElement.addEventListener('focus', this.proxyFocus);
this.anchorElement.tabIndex = -1;
}
}
protected override update(changes: PropertyValues): void {
super.update(changes);
if (changes.has('label')) {
if (this.label) {
this.setAttribute('aria-label', this.label);
} else {
this.removeAttribute('aria-label');
}
}
}
}
18 changes: 13 additions & 5 deletions packages/combobox/src/Combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ import {
ifDefined,
live,
repeat,
when,
} from '@spectrum-web-components/base/src/directives.js';
import '@spectrum-web-components/overlay/sp-overlay.js';
import '@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js';
import '@spectrum-web-components/popover/sp-popover.js';
import '@spectrum-web-components/menu/sp-menu.js';
import '@spectrum-web-components/menu/sp-menu-item.js';
import { PendingStateController } from '@spectrum-web-components/reactive-controllers/src/PendingState.js';
import '@spectrum-web-components/picker-button/sp-picker-button.js';
import { Textfield } from '@spectrum-web-components/textfield';
import type { Tooltip } from '@spectrum-web-components/tooltip';
Expand Down Expand Up @@ -83,6 +83,17 @@ export class Combobox extends Textfield {
@property({ type: String, attribute: 'pending-label' })
public pendingLabel = 'Pending';

public pendingStateController: PendingStateController<this>;

/**
* Initializes the `PendingStateController` for the Combobox component.
* When the pending state changes to `true`, the `open` property of the Combobox is set to `false`.
*/
constructor() {
super();
this.pendingStateController = new PendingStateController(this);
}

@query('slot:not([name])')
private optionSlot!: HTMLSlotElement;

Expand Down Expand Up @@ -415,10 +426,7 @@ export class Combobox extends Textfield {
?required=${this.required}
?readonly=${this.readonly}
/>
${when(
this.pending && !this.disabled && !this.readonly,
this.renderLoader
)}
${this.pendingStateController.renderPendingState()}
`;
}

Expand Down
29 changes: 13 additions & 16 deletions packages/picker/src/Picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
ifDefined,
StyleInfo,
styleMap,
when,
} from '@spectrum-web-components/base/src/directives.js';
import {
property,
Expand All @@ -52,6 +51,7 @@ import {
MatchMediaController,
} from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';
import { DependencyManagerController } from '@spectrum-web-components/reactive-controllers/src/DependencyManger.js';
import { PendingStateController } from '@spectrum-web-components/reactive-controllers/src/PendingState.js';
import { Overlay } from '@spectrum-web-components/overlay/src/Overlay.js';
import type { SlottableRequestEvent } from '@spectrum-web-components/overlay/src/slottable-request-event.js';
import type { FieldLabel } from '@spectrum-web-components/field-label';
Expand Down Expand Up @@ -154,6 +154,17 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
return this._selectedItem;
}

public pendingStateController: PendingStateController<this>;

/**
* Initializes the `PendingStateController` for the Picker component.
* The `PendingStateController` manages the pending state of the Picker.
*/
constructor() {
super();
this.pendingStateController = new PendingStateController(this);
}

public set selectedItem(selectedItem: MenuItem | undefined) {
this.selectedItemContent = selectedItem
? selectedItem.itemChildren
Expand Down Expand Up @@ -422,21 +433,7 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
></sp-icon-alert>
`
: nothing}
${when(this.pending, () => {
import(
'@spectrum-web-components/progress-circle/sp-progress-circle.js'
);
// aria-valuetext is a workaround for aria-valuenow being applied in Firefox even in indeterminate mode.
return html`
<sp-progress-circle
id="loader"
size="s"
indeterminate
aria-valuetext=${this.pendingLabel}
class="progress-circle"
></sp-progress-circle>
`;
})}
${this.pendingStateController.renderPendingState()}
<sp-icon-chevron100
class="picker ${chevronClass[
this.size as DefaultElementSize
Expand Down
62 changes: 62 additions & 0 deletions tools/reactive-controllers/PendingStateController.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Description

The `PendingStateController` is a class that helps manage the pending state of a reactive element. It provides a standardized way to indicate when an element is in a pending state, such as during an asynchronous operation.
When the components is in a pending state it supplies the pending state UI `sp-progress-circle` which gets rendered in the component.
It also updates the value of ARIA label of the host element to its pending-label based on the pending state.

The `HostWithPendingState` interface defines the properties that a host element must implement to work with the `PendingStateController`.

## Usage

[![See it on NPM!](https://img.shields.io/npm/v/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://www.npmjs.com/package/@spectrum-web-components/reactive-controllers)
[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@spectrum-web-components/reactive-controllers?style=for-the-badge)](https://bundlephobia.com/result?p=@spectrum-web-components/reactive-controllers)

```
yarn add @spectrum-web-components/reactive-controllers
```

Import the `PendingStateController` via:

```
import { PendingStateController } from '@spectrum-web-components/reactive-controllers/src/PendingState.js';
```

## Example

```js
import { LitElement } from 'lit';
import { PendingStateController } from '@spectrum-web-components/reactive-controllers/src/PendingState.js';
class Host extends LitElement{

/** Whether the items are currently loading. */
@property({ type: Boolean, reflect: true })
public pending = false;

/** Defines a string value that labels the Picker while it is in pending state. */
@property({ type: String, attribute: 'pending-label' })
public pendingLabel = 'Pending';
public pendingStateController: PendingStateController<this>;

/**
* Initializes the `PendingStateController` for the Picker component.
* The `PendingStateController` manages the pending state of the Component.
*/
constructor() {
super();
this.pendingStateController = new PendingStateController(this);
}
render(){
return html`
<host-element></host-element>
${when(
this.pending,
() => {
return this.pendingStateController.renderPendingState();
}
)}
`
}

}

```
4 changes: 4 additions & 0 deletions tools/reactive-controllers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"development": "./src/MatchMedia.dev.js",
"default": "./src/MatchMedia.js"
},
"./src/PendingState.js": {
"development": "./src/PendingState.dev.js",
"default": "./src/PendingState.js"
},
"./src/RovingTabindex.js": {
"development": "./src/RovingTabindex.dev.js",
"default": "./src/RovingTabindex.js"
Expand Down
Loading

0 comments on commit 68baf94

Please sign in to comment.