UNPKG

color-elements

Version:

A set of web components for working with color. A Color.js project.

306 lines (258 loc) 7.19 kB
import ColorElement from "../common/color-element.js"; import "../gamut-badge/gamut-badge.js"; let importIncrementable; const SUPPORTS_POPOVER = "popover" in HTMLElement.prototype; const Self = class ColorSwatch extends ColorElement { static tagName = "color-swatch"; static url = import.meta.url; static dependencies = new Set(["gamut-badge"]); static styles = import("./color-swatch.css", { with: { type: "css" } }).catch( () => new URL("./color-swatch.css", import.meta.url), ); static shadowTemplate = ` <slot name="swatch"> <div id="swatch" part="swatch"> <slot name="swatch-content"></slot> </div> </slot> <div id="wrapper" part="details"> <slot name="before"></slot> <div part="label"></div> <div part="color"> <slot></slot> </div> <slot name="after"></slot> </div>`; constructor () { super(); this._el = { wrapper: this.shadowRoot.querySelector("#wrapper"), label: this.shadowRoot.querySelector("[part=label]"), colorWrapper: this.shadowRoot.querySelector("[part=color]"), }; this._slots = { default: this.shadowRoot.querySelector("slot:not([name])"), swatch: this.shadowRoot.querySelector("slot[name=swatch]"), }; this._el.swatch = this._slots.swatch.assignedElements()[0] ?? this._slots.swatch.firstElementChild; this.#updateStatic(); this._slots.default.addEventListener("slotchange", evt => this.#updateStatic()); } connectedCallback () { super.connectedCallback(); if (SUPPORTS_POPOVER) { // Focus as well as hover, so the tooltip works by keyboard too (WCAG // 1.4.13: content on hover *or focus*) if the consumer makes the swatch // focusable. for (let type of ["pointerenter", "pointerleave", "focusin", "focusout"]) { this.addEventListener(type, this); } } } /** Whether the details are presented compactly (as a hover/focus tooltip). */ get #compact () { return getComputedStyle(this).getPropertyValue("--details-style").trim() === "compact"; } handleEvent (evt) { let popover = this._el.wrapper; if (!this.#compact) { popover.removeAttribute("popover"); return; } if (!popover.hasAttribute("popover")) { popover.setAttribute("popover", "hint"); } // Show while hovered or focused; dismiss once neither holds. On a leave/blur // event we check the *other* modality, so the tooltip stays open if the user // is still hovering or still focused. let show = this.matches(":focus-within, :hover"); popover.togglePopover({ force: show, source: this._el.swatch }); } #updateStatic () { let previousInput = this._el.input; let input = (this._el.input = this.querySelector("input")); this.static = !input; // This should eventually be a custom state this._el.wrapper.classList.toggle("static", this.static); if (input && input !== previousInput) { importIncrementable ??= import("incrementable").then(m => m.default).catch(); importIncrementable?.then(Incrementable => new Incrementable(input)); input.addEventListener("input", evt => { this.value = evt.target.value; }); } } get gamut () { return this._el.gamutIndicator.gamut; } get swatchTextContent () { // Children that are not assigned to another slot return [...this.childNodes] .filter(n => !n.slot) .map(n => n.textContent) .join("") .trim(); } propChangedCallback ({ name }) { // Synchronous so consumers (e.g., color-chart) can read --color in the same propchange cycle if (name === "color") { this.style.setProperty("--color", this.color?.display()); } } updated ({ changed }) { let input = this._el.input; if (changed.has("gamuts")) { if (this.gamuts === "none") { this._el.gamutIndicator?.remove(); this._el.gamutIndicator = null; } else if (this.gamuts) { if (!this._el.gamutIndicator) { this._el.gamutIndicator = Object.assign(document.createElement("gamut-badge"), { id: "gamut", part: "gamut", exportparts: "label: gamutLabel", gamuts: this.gamuts, color: this.color, }); this.shadowRoot.append(this._el.gamutIndicator); this._el.gamutIndicator.addEventListener("gamutchange", evt => { let gamut = evt.value; this.setAttribute("gamut", gamut); this.dispatchEvent(new evt.constructor("gamutchange", { value: gamut })); }); } else { this._el.gamutIndicator.gamuts = this.gamuts; } } } if (changed.has("value")) { if (input && (!input.value || input.value !== this.value)) { input.value = this.value; } } if (changed.has("label")) { if (this.label.length && this.label !== this.swatchTextContent) { this._el.label.textContent = this.label; this._el.label.title = this.label; } else { this._el.label.textContent = ""; this._el.label.title = ""; } } if (changed.has("color")) { let isValid = this.color !== null || !this.value; input?.setCustomValidity(isValid ? "" : "Invalid color"); if (this._el.gamutIndicator) { this._el.gamutIndicator.color = this.color; } } if (changed.has("colorInfo")) { if (!this.colorInfo) { return; } this._el.info ??= Object.assign(document.createElement("dl"), { part: "info" }); if (!this._el.info.parentNode) { this._el.colorWrapper.after(this._el.info); } let info = []; for (let coord of this.info) { let [label, channel] = Object.entries(coord)[0]; let value = this.colorInfo[channel]; if (value === undefined) { continue; } value = typeof value === "number" ? Number(value.toPrecision(4)) : value; info.push(`<div class="coord"><dt>${label}</dt><dd>${value}</dd></div>`); } this._el.info.innerHTML = info.join("\n"); } } static props = { size: {}, open: {}, gamuts: { default: "srgb, p3, rec2020: P3+, prophoto: PP", }, value: { type: String, default () { if (this._el.input) { return this._el.input.value; } return this.swatchTextContent; }, reflect: { from: true, }, }, label: { type: String, default () { return this.swatchTextContent; }, convert (value) { return value.trim(); }, }, color: { get type () { return ColorSwatch.Color; }, get () { if (!this.value) { return null; } return ColorSwatch.Color.get(this.value); }, set (value) { this.value = ColorSwatch.Color.get(value)?.display(); }, reflect: false, }, info: { type: { is: Array, values: { is: Object, defaultKey: (coord, i) => ColorSwatch.Color.Space.resolveCoord(coord)?.name, }, }, default: [], reflect: { from: true, }, }, colorInfo: { get () { if (!this.info.length || !this.color) { return; } let ret = {}; for (let coord of this.info) { let [label, channel] = Object.entries(coord)[0]; try { ret[channel] = this.color.get(channel); } catch (e) { console.error(e); } } return ret; }, }, }; static events = { colorchange: { propchange: "color", }, valuechange: { propchange: "value", }, }; }; Self.define(); export default Self;