Skip to content

Commit

Permalink
base: disabled; input: focus
Browse files Browse the repository at this point in the history
BaseElement fixes for disabled and tabindex, which is even funkier than I had thought.
InputNum fixes for focus, also funkier than imagined.
  • Loading branch information
sidewayss committed Nov 26, 2024
1 parent 45c88c0 commit 96c4c4d
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 94 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ If you bundle your templates into one file, then each `<template>` must have the
<template id="input-num">...</template>
```

NOTE: If you are using import options and getting an error about an element/tag already being registered, you might need to add the same import options to your JavaScript `import` statements, so that they match your HTML `<script>` imports. The browser can import these as two separate files. This happens sometimes for reasons unknown to me.
NOTE: If you are using import options and getting an error about an element/tag already being registered, you might need to add the same import options to your JavaScript `import` statements, so that they match your HTML `<script>` imports. This happens sometimes because the browser imports them as two separate files, for reasons unknown to me.

### Managing Template Files
There are built-in template files in the `templates/` directory. They serve two purposes:
Expand Down Expand Up @@ -103,7 +103,9 @@ A quick glossary entry of note: A [boolean attribute](https://developer.mozilla.
These classes manage common attributes/properties and behaviors across elements. All the attributes/properties listed are inherited by the sub-classes.
### `class BaseElement extends HTMLElement`
`BaseElement` is the top level class. It is the parent class for `InputNum` and `MultiState`. It manages two global DOM attributes, `disabled` and `tabindex`. See `base-element.js`.
`BaseElement` is the top level class. It is the parent class for `InputNum` and `MultiState`. It manages two global DOM attributes, `disabled` and `tabindex`. See `base-element.js`.`
NOTE: `HTMLElement` does not have a `disabled` attribute/property. It only has `tabindex`/`tabIndex`. `BaseElement` observes the `disabled` and `tabindex` attributes and exposes the properties. To do so requires the `disabled` attribute to manage `tabindex` too, because setting `disabled` *should* set `tabindex="-1"`. This is complicated if you set `tabindex` instead of relying on the default tab order, but most of that can be handled. But if you are setting both `disabled` and `tabindex` in your HTML file, you *must* set `tabindex` before `disabled`. This because the DOM runs `attributeChangedCallback()` in a FIFO queue and when `disabled` sets the `tabindex` it goes to the end of the queue, after the `tabindex` setting in the HTML file. Though it's allowed, setting `tabindex` to anything other than `0` or `-1` is sternly [recommended against by MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) (*scroll down the page to the first Warning block*).
### `class MultiState`
`MultiState` (`multi-state.js`) is the parent class for `<state-btn>` and `MultiCheck`.
Expand Down
20 changes: 8 additions & 12 deletions apps/input-num/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,20 +207,21 @@ function change(evt) {
case elms.min:
minDigits();
case elms.max: // validate
setInfo(elms.max, inNum.max <= inNum.min, "Max/Min overlap!");
disable(elms.autoWidth, inNum.max == Infinity
|| inNum.min == -Infinity);
setInfo(elms.max, inNum.max <= inNum.min, "Max/Min overlap!");
elms.autoWidth.disabled = inNum.max == Infinity
|| inNum.min == -Infinity;
break;
case elms.locale:
disable(elms.currency, !val);
elms.currency.disabled = !val;
elms.currency.labels[0].classList.toggle("disabled", !val);
case elms.currency:
disable(elms.accounting, !val || !inNum.currency
|| !inNum.useLocale);
elms.accounting.disabled = !val || !inNum.currency
|| !inNum.useLocale;
case elms.units: // display info
setInfo(tar, val, g[id][val]);
break;
case elms.spins: // disable step, delay, interval
spinner.forEach(elm => disable(elm, !val));
spinner.forEach(elm => elm.disabled = !val);
case elms.keyboards:
setInfo(elms.accounting,
!elms.spins.checked && !elms.keyboards.checked,
Expand All @@ -239,11 +240,6 @@ function change(evt) {
function setInfo(elm, b, text) { // sets info/warning text to the right of elm
elm.nextElementSibling.innerHTML = b ? text : "";
}
function disable(elm, b) { // disables an element and its label
elm.disabled = b;
for (const lbl of [elm.previousElementSibling, elm.nextElementSibling])
lbl?.classList.toggle("disabled", b);
}
function minDigits() { // one alert set in two places
setInfo(elms.min,
inNum.min && Math.abs(inNum.min) < 1 / Math.pow(base, inNum.digits),
Expand Down
39 changes: 26 additions & 13 deletions base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@ VALUE = "value"; // exported: defined here but not handled here
// =============================================================================
// The custom base class, direct sub-class of HTMLElement
class BaseElement extends HTMLElement {
#id; #meta; #tabIndex;
#connected; #disabled; #disabling; #id; #meta; #ptrEvents; #tabIndex;
static searchParams; // set by html-elements.js, if it's used
static observedAttributes = [DISABLED, TAB_INDEX];
static promises = new Map; // for noAwait, see https://github.com/sidewayss/html-elements/issues/8
constructor(meta, template, noAwait) {
super();
if (noAwait) { // passes subclass as template arg
if (noAwait) { // passes subclass as template arg
this.#meta = meta;
this.#id = classToTag(template);
BaseElement.promises.set(this, promise());
}
else
this.#attach(template); // the <template> as DocumentFragment
this.#attach(template); // <template> as DocumentFragment

if (this.tabIndex < 0)
this.tabIndex = 0;
this.#tabIndex = this.tabIndex;
this.setAttribute(TAB_INDEX, "0"); // default value emulates <input>
}
#attach(template) {
this._dom = this.attachShadow({mode:"open"});
Expand All @@ -42,28 +40,43 @@ class BaseElement extends HTMLElement {
}
else
this._connected?.();

// Creating the disabled attribute from scratch is complicated
this.#disabled = this.getAttribute(DISABLED);
this.#connected = true;
}
// attributeChangedCallback() handles changes to the observed attributes
attributeChangedCallback(name, _, path) {
attributeChangedCallback(name, _, val) {
switch (name) {
case DISABLED:
if (path !== null) { // null == removeAttribute()
this.tabIndex = -1;
if (this.#connected) {
if (val === this.#disabled)
return;
else //-------------------
this.#disabled = val;
}
this.#disabling = true;
if (val !== null) { // null == removeAttribute()
this.tabIndex = -1; // indirectly recursive
this.#ptrEvents = this.style.pointerEvents;
this.style.pointerEvents = "none";
}
else {
this.tabIndex = this.#tabIndex;
this.style.pointerEvents = "";
this.style.pointerEvents = this.#ptrEvents;
}
break;
return;
case TAB_INDEX:
this.#tabIndex = path; // to restore post-disable
if (this.#disabling) // easier done here, not case DISABLED
this.#disabling = false;
else // to restore post-disable
this.#tabIndex = val;
default:
}
}
// getters/setters reflect the HTML attributes, see attributeChangedCallback()
get disabled() { return this.hasAttribute(DISABLED); }
set disabled(path) { this._setBool(DISABLED, path); }
set disabled(val) { this._setBool(DISABLED, val); }

// define wraps customElements.define for consistency of class and tag names
static define(cls) {
Expand Down
Binary file modified html-elements.xlsx
Binary file not shown.
Loading

0 comments on commit 96c4c4d

Please sign in to comment.