Skip to content

Conversation

@lukewarlow
Copy link

@lukewarlow lukewarlow commented Feb 27, 2025

I have an initial implementation to go along with this RFC. More thought is needed in terms of Custom Properties for colours etc.

const styles = `
@property --width {
    syntax: "<length>";
    inherits: false;
    initial-value: 0;
  }

  :host {
    display: inline-block;
    width: 200px;
    height: 10px;
  }

  :host::part(track) {
    width: 100%;
    height: 100%;
    border: thin solid light-dark(black, white);
    overflow: hidden;
    border-radius: 20px;
  }

  :host::part(fill) {
    width: var(--width, 0);
    height: 100%;
    background-color: light-dark(blue, lightblue);
  }

  :host(:state(indeterminate))::part(fill) {
    width: 20%;
    animation: progress 2.5s ease-in-out;
    animation-iteration-count: infinite;
    animation-fill-mode: both;
    position: relative;
    border-radius: inherit;
  }

  @keyframes progress {
    0% {
      left: 0;
    }
    50% {
      left: 80%;
    }
    100% {
      left: 0;
    }
  }
`;

const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(styles);

class OuiProgress extends HTMLElement {
  static formAssociated = true;
  static observedAttributes = ["value", "max"];

  #shadowRoot;
  #internals;

  #updateState() {
    this.#internals.ariaValueMin = 0;
    this.#internals.araiValueMax = this.max;
    this.#internals.states.clear();

    if (
      !this.attributes["value"] ||
      Number.isNaN(parseFloat(this.attributes["value"].value))
    ) {
      this.#internals.states.add("indeterminate");
    } else {
      this.#internals.ariaValueNow = this.value;
    }

    this.style.setProperty("--width", `${this.position * 100}%`);
  }

  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#internals.role = "progressbar";
  }

  connectedCallback() {
    this.#shadowRoot = this.attachShadow({ mode: "closed" });
    this.#shadowRoot.adoptedStyleSheets = [stylesheet];
    const track = document.createElement('div');
    track.part = 'track';
    const fill = document.createElement('div');
    fill.part = 'fill';
    track.appendChild(fill);
    this.#shadowRoot.appendChild(track);
    this.#updateState();
  }

  attributeChangedCallback() {
    this.#updateState();
  }

  get value() {
    let value = parseFloat(this.attributes["value"]?.value);
    if (Number.isNaN(value) || value < 0) {
      value = 0;
    }

    return Math.min(value, this.max);
  }

  set value(value) {
    this.setAttribute("value", value?.toString());
  }

  get max() {
    let value = parseFloat(this.attributes["max"]?.value);
    if (Number.isNaN(value) || value <= 0) {
      value = 1;
    }

    return value;
  }

  set max(value) {
    this.setAttribute("max", value?.toString());
  }

  get position() {
    if (this.#internals.states.has("indeterminate")) {
      return -1;
    }

    return this.value / this.max;
  }
}

window.customElements.define("oui-progress", OuiProgress);

@lukewarlow lukewarlow marked this pull request as ready for review February 27, 2025 20:09
Comment on lines +62 to +64
| Slot | Description |
|------|-------------------------------------------------------------------|
| `default` | Fallback text to render if the component otherwise fails to load. |
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to expose a slot for content in the filled in portion of the progress bar (e.g. a percentage number)?

Suggested change
| Slot | Description |
|------|-------------------------------------------------------------------|
| `default` | Fallback text to render if the component otherwise fails to load. |
| Slot | Description |
|------|-------------------------------------------------------------------|
| `default` | Fallback text to render if the component otherwise fails to load. |
| `fill` | Optional content for the filled in portion of the progress bar. |

@gfellerph
Copy link
Collaborator

Just wanted to see what the proposal looks like: https://codepen.io/tuelsch/pen/EaxoaJx?editors=1010

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants