-
Notifications
You must be signed in to change notification settings - Fork 220
Description
NOTE: this issue was originally written when the popup proposal still included a <popup> element. It has since been edited, but there may be some inconsistencies
The problem
The current Popup explainer seems to be missing some imperative API docs for certain features such as the anchor and togglepopup attributes.
The background
The reason why an imperative API would help is that ID's for associating elements leads into multiple problems:
- IDs do not penetrate shadow roots
- componentizing a popup button or a popup itself will require the component author to generate IDs on the fly
For example, shadow roots:
<button togglepopup="my-popup" id="my-anchor">toggle the popup</button>
<my-popup>
<template shadowroot="open">
<div popup id="my-popup" anchor="my-anchor">
<slot></slot>
</div>
</template>
</my-popup>The above example has an anchor with id my-anchor and a popup inside of a shadow root (using the declarative shadow dom api). Adding [anchor=my-anchor] to div[popup] will not work as ID's cannot penetrate shadow roots, and button[togglepopup=my-popup] will also not work because it won't be able to penetrate the shadow root to find the popup.
Example ID generation:
// PopupButton.jsx
import React, { useState } from "react";
let idCounter = 0;
export default () => {
const [currentId] = useState(idCounter++);
return (
<>
<button togglepopup={`popup-button-${currentId}`}>
Click me to open popup
</button>
<div popup id={`popup-button-${currentId}`}>...</div popup>
</>
);
};The above example in react would require generating an ID to link a popup button with a popup (same situation would exist with anchor) when an imperative solution (like a ref or a function callback) would suffice.
Proposed solution
Defining an imperative way to associate element's popup or anchor.
I see two options here:
- Allowing
anchorandtogglepopupproperties onHTMLElementto support element references as well as string - Creating a setter method for
setAnchor(el: HTMLElement)andtogglePopUp(el: HTMLPopupElement)
Both of these options solve the problems above, but have different tradeoffs.
Allowing property to support Element references
The positives about this approach is that it will allow for better declarative programming on modern templating frameworks such as React and lit-html. For example:
React:
// PopupButton.jsx
import React, { useRef } from "react";
export default ({anochorIdOrRef} /* type is string|HTMLElement|undefined */) => {
const [popupRef] = useRef(undefined);
return (
<>
<button togglepopup={popupRef.current}>
Click me to open popup
</button>
<div popup ref={popupRef} anchor={anochorIdOrRef}>...</div>
</>
);
};
// usage: <PopupButton anchorIdOrRef="my-anchor" />Lit:
note: the .propName=${val} syntax denotes a property binding not an attribute binding e.g. el.propName = val rather than el.setAttribute('propName', val)
// popup-button.js
import {LitElement, html} from 'lit';
class PopupButton extends LitElement {
get popupEl() { this.shadowRoot?.querySelector('popup') ?? undefined; }
static properties = {anchorEl: {attribute: false}};
anchorEl = undefined; // type is HTMLElement|undefined
render() {
return html`
<button .togglepopup=${this.popupEl}>
Click me to open popup
</button>
<div popup .anchor=${this.anchorEl}>...</div>`;
}
}
customElements.define('popup-button', PopupButton);
// usage: <popup-button .anchor=${document.body.querySelector('#my-anchor')}></popup-button>The negatives are that there is current no precedent that I know of for an element property accepting both a string, or an element reference.
Function setters
The Positives about this approach are that it will enable all these use cases and it fits with precedent (not having a single property accept HTMLElement or string)
The Negatives requires component authors to wire things up a bit more. e.g.
React:
// PopupButton.jsx
import React, { useRef, useCallback } from "react";
export default ({anchorIdOrRef}) => {
const popupRef = useRef();
const buttonRef = useCallback(buttonEl => {
buttonEl.togglePopUp(popupRef.current);
}, [popupRef]);
useCallback(() => {}, []);
return (
<>
<button ref={buttonRef}>
Click me to open popup
</button>
<div popup ref={popupRef} anchor={anchorIdOrRef}>...</div>
</>
);
};Lit:
// popup-button.js
import {LitElement, html} from 'lit';
class PopupButton extends LitElement {
get buttonEl() { this.shadowRoot?.querySelector('button') ?? undefined; }
get popupEl() { this.shadowRoot?.querySelector('popup') ?? undefined; }
static properties = {anchorEl: {attribute: false}};
anchorEl = undefined;
render() {
return html`
<button>
Click me to open popup
</button>
<div popup>...</div popup>`;
}
firstUpdated() {
this.buttonEl.togglePopUp(this.popupEl);
}
updated(changed) {
if (changed.has('anchorEl')) {
this.popupEl.setAnchor(this.anchorEl);
}
}
}
customElements.define('popup-button', PopupButton);Acceptance criteria
Some sort of element-reference imperative API for anchor is defined, or something that makes it easy to componentize HTMLElement[popup], especially in Shadow DOM