Packages
noora
0.85.0
0.85.0
0.84.1
0.84.0
0.83.2
0.83.1
0.83.0
0.82.7
0.82.6
0.82.5
0.82.4
0.82.3
0.82.2
0.82.1
0.82.0
0.81.4
0.81.3
0.81.2
0.81.1
0.81.0
0.80.1
0.80.0
0.79.1
0.79.0
0.78.1
0.78.0
0.77.4
0.77.3
0.77.2
0.77.1
0.77.0
0.76.0
0.75.0
0.74.0
0.73.0
0.72.0
0.71.0
0.70.0
0.69.1
0.69.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.2
0.64.1
0.64.0
0.63.2
0.63.1
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.1
0.53.0
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.3
0.44.2
0.44.1
0.44.0
0.43.0
0.42.0
0.41.0
0.40.6
0.40.5
0.40.4
0.40.3
0.40.2
0.40.1
0.40.0
0.39.1
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.1
0.32.0
0.31.0
0.30.0
0.29.2
0.29.1
0.29.0
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.0
0.26.1
0.26.0
0.25.0
0.24.0
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.1
0.12.0
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.0
0.1.0
0.1.0-rc.2
0.1.0-rc.1
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
A component library for Phoenix LiveView applications
Current section
Files
Jump to
Current section
Files
js/web-components/Button.js
import { LitElement, css, html, unsafeCSS } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import buttonStyles from "../../css/button.css";
import buttonContract from "../../components/button.json";
import {
contractAttribute,
contractProperties,
initializeContractDefaults,
resolveContractValue,
} from "./Contract.js";
const sizeAttribute = contractAttribute(buttonContract, "size");
const typeAttribute = contractAttribute(buttonContract, "type");
const variantAttribute = contractAttribute(buttonContract, "variant");
export class NooraButton extends LitElement {
static formAssociated = true;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
static properties = contractProperties(buttonContract);
static styles = [
unsafeCSS(buttonStyles),
css`
slot::slotted(svg) {
width: var(--noora-button-icon-size);
height: var(--noora-button-icon-size);
pointer-events: none;
}
`,
];
#formDisabled = false;
#internals;
#restoreFocus = false;
#submitProxy;
constructor() {
super();
this.#internals = this.attachInternals?.();
initializeContractDefaults(this, buttonContract);
}
connectedCallback() {
super.connectedCallback();
this.#syncAccessibilityReferences();
this.#syncSubmitProxy();
}
disconnectedCallback() {
super.disconnectedCallback();
queueMicrotask(() => {
if (!this.isConnected) this.#submitProxy?.remove();
});
}
get form() {
return (
this.#internals?.form ?? this.#formFromAttribute() ?? this.closest("form")
);
}
get control() {
return this.renderRoot?.querySelector('[part="control"]') ?? null;
}
click() {
this.control?.click();
}
focus(options) {
this.control?.focus(options);
}
formDisabledCallback(disabled) {
this.#formDisabled = disabled;
if (this.#submitProxy) this.#submitProxy.disabled = this.#isDisabled();
this.requestUpdate();
}
willUpdate() {
this.#restoreFocus = this.control === this.renderRoot?.activeElement;
}
updated() {
if (this.#restoreFocus) this.control?.focus();
this.#syncAccessibilityReferences();
this.#syncSubmitProxy();
}
render() {
const content = this.iconOnly
? html`<slot></slot>`
: html`
<slot name="icon-left"></slot>
<span><slot>${this.label}</slot></span>
<slot name="icon-right"></slot>
`;
if (this.href && !this.#isDisabled()) {
return html`
<a
class=${buttonContract.className}
part="control"
href=${this.href}
data-variant=${this.#variant()}
data-size=${this.#size()}
?data-icon-only=${this.iconOnly}
aria-describedby=${ifDefined(this._ariaDescribedBy)}
aria-label=${ifDefined(this._ariaLabel)}
aria-labelledby=${ifDefined(this._ariaLabelledBy)}
download=${ifDefined(this.download)}
rel=${ifDefined(this.rel)}
target=${ifDefined(this.target)}
title=${ifDefined(this._title)}
>
${content}
</a>
`;
}
return html`
<button
class=${buttonContract.className}
part="control"
type=${this.#type()}
name=${this.name}
value=${this.value}
data-variant=${this.#variant()}
data-size=${this.#size()}
?data-icon-only=${this.iconOnly}
?autofocus=${this.autofocus}
?disabled=${this.#isDisabled()}
aria-describedby=${ifDefined(this._ariaDescribedBy)}
aria-label=${ifDefined(this._ariaLabel)}
aria-labelledby=${ifDefined(this._ariaLabelledBy)}
form=${ifDefined(this.formId)}
formaction=${ifDefined(this.formAction)}
formenctype=${ifDefined(this.formEnctype)}
formmethod=${ifDefined(this.formMethod)}
?formnovalidate=${this.formNoValidate}
formtarget=${ifDefined(this.formTarget)}
title=${ifDefined(this._title)}
@click=${this.#handleButtonActivation}
>
${content}
</button>
`;
}
#handleButtonActivation(event) {
if (this.#isDisabled() || this.#type() === "button") return;
queueMicrotask(() => {
if (event.defaultPrevented) return;
const form = this.form;
if (!form) return;
if (this.#type() === "reset") {
form.reset();
return;
}
this.#syncSubmitProxy();
this.#submitProxy?.form?.requestSubmit(this.#submitProxy);
});
}
#syncAccessibilityReferences() {
const control = this.control;
const root = this.getRootNode();
if (!control || !("getElementById" in root)) return;
for (const [property, value] of [
["ariaDescribedByElements", this._ariaDescribedBy],
["ariaLabelledByElements", this._ariaLabelledBy],
]) {
if (!(property in control)) continue;
control[property] =
value
?.trim()
.split(/\s+/)
.map((id) => root.getElementById(id))
.filter(Boolean) ?? [];
}
}
#syncSubmitProxy() {
if (this.href || this.#type() !== "submit") {
this.#submitProxy?.remove();
return;
}
if (!this.#submitProxy) {
this.#submitProxy = this.ownerDocument.createElement("button");
this.#submitProxy.hidden = true;
this.#submitProxy.tabIndex = -1;
this.#submitProxy.type = "submit";
this.#submitProxy.setAttribute("aria-hidden", "true");
}
for (const [attribute, value] of [
["form", this.formId],
["formaction", this.formAction],
["formenctype", this.formEnctype],
["formmethod", this.formMethod],
["formtarget", this.formTarget],
["name", this.name],
["value", this.value],
]) {
if (value === undefined || value === null) {
this.#submitProxy.removeAttribute(attribute);
} else {
this.#submitProxy.setAttribute(attribute, value);
}
}
this.#submitProxy.disabled = this.#isDisabled();
this.#submitProxy.formNoValidate = this.formNoValidate;
if (this.parentNode && this.nextSibling !== this.#submitProxy) {
this.parentNode.insertBefore(this.#submitProxy, this.nextSibling);
}
}
#formFromAttribute() {
if (!this.formId) return null;
const form = this.ownerDocument.getElementById(this.formId);
return form instanceof HTMLFormElement ? form : null;
}
#isDisabled() {
return this.disabled || this.#formDisabled;
}
#size() {
return resolveContractValue(this.size, sizeAttribute);
}
#type() {
return resolveContractValue(this.type, typeAttribute);
}
#variant() {
return resolveContractValue(this.variant, variantAttribute);
}
}
export function registerNooraButton() {
if (!customElements.get(buttonContract.tagName)) {
customElements.define(buttonContract.tagName, NooraButton);
}
}