@fusion.dev/helix-core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/custom-elements.json +4 -4
  2. package/dist/controllers/aria-forward-controller/aria-forward-controller.js +0 -105
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +0 -71
  4. package/dist/controllers/command-controller/command-controller.js +0 -62
  5. package/dist/controllers/copy-controller/copy-controller.js +0 -63
  6. package/dist/controllers/disabled-cascade-controller/disabled-cascade-controller.js +0 -40
  7. package/dist/controllers/dismiss-controller/dismiss-controller.js +0 -124
  8. package/dist/controllers/drilldown-controller/drilldown-controller.js +0 -87
  9. package/dist/controllers/floating-controller/floating-controller.js +0 -152
  10. package/dist/controllers/focus-trap-controller/focus-trap-controller.js +0 -35
  11. package/dist/controllers/indicator-controller/indicator-controller.js +0 -82
  12. package/dist/controllers/keyboard-navigation-controller/keyboard-navigation-controller.js +0 -46
  13. package/dist/controllers/popover-controller/popover-controller.js +0 -72
  14. package/dist/controllers/source-controller/source-controller.js +0 -38
  15. package/dist/controllers/token-color-controller/token-color-controller.js +0 -51
  16. package/dist/controllers/virtualize-controller/virtualize-controller.js +0 -128
  17. package/dist/events/element-events.js +0 -22
  18. package/dist/events/events.js +0 -386
  19. package/dist/helix-element/helix-element.d.ts +10 -17
  20. package/dist/helix-element/helix-element.js +0 -505
  21. package/dist/helix-form-element/helix-form-element.js +0 -140
  22. package/dist/mixins/mixin-chart-source/mixin-chart-source.js +0 -77
  23. package/dist/mixins/mixin-collection/mixin-collection.js +0 -83
  24. package/dist/mixins/mixin-controller-active/mixin-controller-active.js +0 -82
  25. package/dist/mixins/mixin-controller-checked/mixin-controller-checked.js +0 -78
  26. package/dist/mixins/mixin-controller-open/mixin-controller-open.js +0 -93
  27. package/dist/mixins/mixin-controller-value/mixin-controller-value.js +0 -87
  28. package/dist/mixins/mixin-dismissible/mixin-dismissible.js +0 -38
  29. package/dist/mixins/mixin-field/mixin-field.js +0 -277
  30. package/dist/utils/anchor-element/anchor-element.js +0 -23
  31. package/dist/utils/capture-pointer/capture-pointer.js +0 -18
  32. package/dist/utils/closest-instance/closest-instance.js +0 -20
  33. package/dist/utils/create-sheet/create-sheet.js +0 -14
  34. package/dist/utils/custom-element/custom-element.js +0 -83
  35. package/dist/utils/draw-icon/draw-icon.js +0 -28
  36. package/dist/utils/draw-tooltip/draw-tooltip.js +1 -50
  37. package/dist/utils/elapsed-time/elapsed-time.js +0 -51
  38. package/dist/utils/focusable-elements/focusable-elements.js +1 -59
  39. package/dist/utils/format-numbers/format-numbers.js +0 -16
  40. package/dist/utils/host-aria/host-aria.js +0 -26
  41. package/dist/utils/inherit-disabled/inherit-disabled.js +0 -36
  42. package/dist/utils/line-path/line-path.js +0 -36
  43. package/dist/utils/on-unless-false/on-unless-false.js +0 -23
  44. package/dist/utils/read-series/read-series.js +0 -71
  45. package/dist/utils/relative-time/relative-time.js +0 -43
  46. package/dist/utils/resolve-active-item/resolve-active-item.js +0 -20
  47. package/dist/utils/restore-value/restore-value.js +0 -54
  48. package/dist/utils/scale/scale.js +0 -19
  49. package/dist/utils/scale/ticks.js +0 -36
  50. package/dist/utils/scroll-item-into-view/scroll-item-into-view.js +0 -53
  51. package/dist/utils/slot-filled/slot-filled.js +0 -23
  52. package/dist/utils/tell-disclosure/tell-disclosure.js +0 -53
  53. package/dist/utils/ticker/ticker.js +0 -22
  54. package/dist/utils/tone-marks/tone-marks.js +0 -24
  55. package/dist/utils/value-as/value-as.js +0 -56
  56. package/package.json +5 -5
@@ -6,161 +6,27 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { property, state } from 'lit/decorators.js';
8
8
  import { HelixElement } from '../helix-element/helix-element.js';
9
- /**
10
- * @summary Base for every element that participates in a form.
11
- *
12
- * @description
13
- * This has to be a class rather than a mixin or controller, and that is not a
14
- * style preference: the browser reads `formAssociated` off the constructor
15
- * during element upgrade, which cannot be delegated to a composed object.
16
- *
17
- * `internals` comes from `HelixElement`, which attaches it for every component.
18
- * `attachInternals()` throws on a second call, so there is exactly one owner.
19
- *
20
- * Deliberately small. It carries what every form participant needs and nothing
21
- * more; `value`, validity, and the reset and restore callbacks land when the
22
- * first input does, rather than being designed against components that do not
23
- * exist yet.
24
- *
25
- * @since 0.1.0
26
- * @status experimental
27
- * @documentation https://helix.fusion.dev/core/form-element
28
- */
29
9
  export class HelixFormElement extends HelixElement {
30
10
  constructor() {
31
11
  super(...arguments);
32
- /**
33
- * Whether the control is non-interactive.
34
- *
35
- * Reflected, because the styled tier selects on it, and mirrored to
36
- * `aria-disabled` rather than removing the element from the accessibility
37
- * tree, so a screen reader can still announce that the control exists.
38
- *
39
- * The mirror itself is in `willUpdate` below, here rather than in each
40
- * control, because here is where `disabled` is.
41
- */
42
12
  this.disabled = false;
43
- /**
44
- * Whether an ancestor `<fieldset disabled>` has switched this control off.
45
- *
46
- * The platform tells a form-associated element through `formDisabledCallback`
47
- * and nothing else does, so without this a control inside a disabled fieldset
48
- * stays fully typeable and announces itself as enabled.
49
- *
50
- * The submission half is already the platform's: measured, the fieldset's
51
- * controls are absent from the form's entry list and the host matches
52
- * `:disabled`, so the value never goes anywhere. What is missing without this
53
- * is everything a reader can see or touch.
54
- *
55
- * State rather than the `disabled` property. Writing that would reflect an
56
- * attribute the consumer's server markup does not have, which React reports
57
- * on hydration and does not patch up, and it would also silently overwrite
58
- * what the consumer set.
59
- */
60
13
  this.formDisabled = false;
61
- /**
62
- * Name submitted with the form.
63
- *
64
- * **Reflected, and it is the exception to "reflect only what CSS selects
65
- * on".** The platform builds a form's entry list from the `name` *content
66
- * attribute*, not from the property, so a control named through the property
67
- * submitted under no name at all and was silently dropped. Measured: two
68
- * inputs in one form, one named by attribute and one by property, and the
69
- * form carried a single entry.
70
- *
71
- * Safe for a React consumer, which is what the rule is protecting: React
72
- * renders a custom element's props as attributes on the server, so the name
73
- * is already in the markup being hydrated and reflecting it writes what is
74
- * there. It is only a change to the markup when a consumer assigns it
75
- * imperatively later, and nothing is hydrating by then.
76
- *
77
- * Here rather than on each control.
78
- */
79
14
  this.name = '';
80
15
  }
81
- /** Opts the element into form ownership, labels, and submission. */
82
16
  static { this.formAssociated = true; }
83
- /**
84
- * Whether the control is off, by its own attribute or by an ancestor.
85
- *
86
- * What a component should bind to rather than `disabled`, which is only the
87
- * half of it the consumer wrote.
88
- *
89
- * @internal Read by the component itself and by nothing outside it: every
90
- * call in the library is `this.isDisabled`. Public, it prints a row on
91
- * twenty-four families in both tiers, forty-eight in all, on every form
92
- * control's reference page.
93
- *
94
- * **A consumer loses nothing, which was measured rather than assumed.**
95
- * `element.matches(':disabled')` answers the same question, is the platform's
96
- * own, and works on a native control too. Checked in a browser across the
97
- * three cases: a control disabled by its own attribute, one inside a
98
- * `<fieldset>` before and after that fieldset is disabled, and one that is
99
- * neither. `:disabled` agreed with `isDisabled` in every one, including the
100
- * inheritance case that is the only reason this getter exists.
101
- *
102
- * Documented away rather than made `protected`, because a subclass in
103
- * `helix/styled` binds to it and that is exactly what it is for.
104
- */
105
17
  get isDisabled() {
106
18
  return this.disabled || this.formDisabled;
107
19
  }
108
- /**
109
- * The platform's notice that an ancestor fieldset changed.
110
- *
111
- * **The argument is the effective state, and taking it whole is what latched
112
- * every control in the library.** The platform calls this for the element's
113
- * own `disabled` as well as for a fieldset, and our own arrives here at the
114
- * worst possible moment: reflecting `disabled` to the attribute happens
115
- * inside Lit's `update()`, the UA fires this synchronously from that write,
116
- * and a reactive write during an update is a change-in-update Lit does not
117
- * re-render for. So `formDisabled` ran exactly one update behind forever.
118
- *
119
- * Measured on Slider, logging what `render` saw:
120
- *
121
- * ```
122
- * set true disabled true formDisabled false isDisabled true
123
- * set false disabled false formDisabled true isDisabled true <- stale
124
- * (no third render)
125
- * ```
126
- *
127
- * The host came out right every time, `disabled` false, `:disabled` not
128
- * matching, and the inner control kept the attribute, so a control disabled
129
- * once could never be re-enabled. It only shows when something re-enables,
130
- * which is the same shape of bug `inheritDisabled` was written for.
131
- *
132
- * Our own half is already in `this.disabled` and `isDisabled` is an or, so
133
- * this records only what the fieldset adds. That also removes the write
134
- * entirely on the own-disabled path rather than moving it.
135
- *
136
- * The one case this cannot see is a control that is disabled itself *and*
137
- * inside a disabled fieldset, whose own `disabled` is then removed: the
138
- * effective state does not change, so the platform does not call again, and
139
- * the fieldset's hold is not recorded. There is no callback to learn it from
140
- * and reading `:disabled` here would answer about the attribute we are in the
141
- * middle of removing.
142
- */
143
20
  formDisabledCallback(disabled) {
144
21
  this.formDisabled = disabled && !this.disabled;
145
22
  }
146
23
  willUpdate(changed) {
147
24
  super.willUpdate(changed);
148
- // Off for either reason, and said once for every control in the library.
149
25
  this.internals.ariaDisabled = this.isDisabled ? 'true' : null;
150
26
  }
151
- /**
152
- * The form this control belongs to, if any.
153
- *
154
- * @reference https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-elements form-associated custom elements
155
- */
156
27
  get form() {
157
28
  return this.internals.form;
158
29
  }
159
- /**
160
- * Labels associated with this control.
161
- *
162
- * @reference https://html.spec.whatwg.org/multipage/custom-elements.html#form-associated-custom-elements form-associated custom elements
163
- */
164
30
  get labels() {
165
31
  return this.internals.labels;
166
32
  }
@@ -173,12 +39,6 @@ __decorate([
173
39
  ], HelixFormElement.prototype, "formDisabled", void 0);
174
40
  __decorate([
175
41
  property({
176
- /* Empty means absent, which plain reflection cannot say. `name = ''` is the
177
- default and Lit reflects it as `name=""`, so every button, switch and
178
- calendar in the library would ship an empty name attribute the consumer
179
- never wrote. Returning `null` removes the attribute instead. Same trap
180
- `hostAria` exists for: a name of no characters is not the absence of
181
- one. */
182
42
  converter: {
183
43
  fromAttribute: (value) => value ?? '',
184
44
  toAttribute: (value) => (value === '' ? null : value),
@@ -8,111 +8,34 @@ import { property } from 'lit/decorators.js';
8
8
  import { ChartPointerController } from '../../controllers/chart-pointer-controller/chart-pointer-controller.js';
9
9
  import { SourceController } from '../../controllers/source-controller/source-controller.js';
10
10
  import { readRows, readSeries } from '../../utils/read-series/read-series.js';
11
- /**
12
- * How many colors the series palette has, and so where it starts over.
13
- *
14
- * Here rather than in each chart, because a number two files have to agree on
15
- * is a number that will stop agreeing: when the palette went from eight to
16
- * twenty-four, one of five copies was missed and drew every ninth series in the
17
- * first color while the legend beside it named the ninth.
18
- */
19
11
  export const PALETTE = 24;
20
- /** The palette slot for the nth series, counting from one and wrapping. */
21
12
  export const seriesIndex = (depth) => (depth % PALETTE) + 1;
22
- /**
23
- * The half of a chart that is about finding its numbers rather than drawing.
24
- *
25
- * **A mixin rather than a controller**, by the rule the tier notes give: it
26
- * introduces `for` and `label`, which a consumer writes. `SourceController` is
27
- * the other half and stays a controller, because resolving an id and watching
28
- * for changes is behavior nobody calls.
29
- *
30
- * **`Legend` deliberately does not take this.** It reads a table and has no
31
- * drawing to title, so `label` would be API it never uses, and a mixin applied
32
- * for four fifths of a fit is how a shared thing starts drifting.
33
- */
34
13
  export const MixinChartSource = (Base) => {
35
14
  class ChartSourceElement extends Base {
36
15
  constructor() {
37
16
  super(...arguments);
38
- /** Resolves `for` and re-renders when the table behind it changes. */
39
17
  this.source = new SourceController(this);
40
- /**
41
- * Which reading a pointer is nearest, for a tooltip to sit on.
42
- *
43
- * Public for the same reason `series` and `named` are: a protected member
44
- * cannot be cast onto across unrelated classes, so it would be invisible to
45
- * every chart that needs it. Reading where a pointer is resting is a fair
46
- * thing to ask a chart anyway.
47
- */
48
18
  this.pointer = new ChartPointerController(this);
49
- /**
50
- * The id of the table to read.
51
- *
52
- * Resolved through `getRootNode()`, so a chart inside another component's
53
- * shadow root names a table in that root rather than in the document. A
54
- * `<table>` or anything built from grid roles, including `helix-table`.
55
- *
56
- * **In React this arrives as a property and not as an attribute**, the way
57
- * `Tooltip.for` does, so it does not reflect and nothing styles on it.
58
- */
59
19
  this.for = '';
60
- /**
61
- * The drawing's name, for a pointer resting on it.
62
- *
63
- * Absent falls back to the table's own caption, which is usually the right
64
- * words and is already written.
65
- */
66
20
  this.label = '';
67
21
  }
68
- /**
69
- * Re-resolves when the chart is pointed at a different table.
70
- *
71
- * `SourceController` watches whatever it resolved on connect, and nothing
72
- * tells it the id changed. Without this, pointing a chart at a second table
73
- * leaves it drawing the first, silently.
74
- */
75
22
  willUpdate(changed) {
76
23
  super.willUpdate(changed);
77
24
  if (changed.has('for')) {
78
25
  this.source.watch();
79
26
  }
80
27
  }
81
- /** The columns of the table, one series each, or none if there is no table. */
82
28
  get series() {
83
29
  const element = this.source.element;
84
30
  return element ? readSeries(element) : [];
85
31
  }
86
- /**
87
- * What to call the drawing, the consumer's word or the table's own.
88
- *
89
- * The fallback is the reason this is shared rather than copied: a caption is
90
- * already written, already the right words, and already the thing a reader
91
- * of the table sees. A chart that made the consumer say it twice would be
92
- * asking for the second one to drift.
93
- */
94
32
  get named() {
95
33
  return this.label || this.source.element?.querySelector('caption')?.textContent?.trim() || '';
96
34
  }
97
- /**
98
- * Every row of the table, with each cell's text as well as its number.
99
- *
100
- * This is where a tooltip's content comes from, and it is deliberately not
101
- * `series`. A chart draws numbers; a reader resting a pointer on one wants
102
- * what the table says, which is `$128k` and not `128000`, and often wants
103
- * the column beside it that nothing plotted at all.
104
- *
105
- * **Not `rows`.** It is the source table's rows, and `rows` on a chart is a
106
- * name a chart wants for itself: `helix-heatmap` takes `rows="7"` to mean a
107
- * calendar week, and declaring that against a base class whose `rows` is a
108
- * `Row[]` getter is a type error at best. Name the member for what it holds
109
- * rather than for the shortest word available.
110
- */
111
35
  get sourceRows() {
112
36
  const element = this.source.element;
113
37
  return element ? readRows(element) : [];
114
38
  }
115
- /** How many readings there are, which is what the pointer maps onto. */
116
39
  get readings() {
117
40
  return this.sourceRows.length;
118
41
  }
@@ -6,74 +6,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { html, nothing } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
- /**
10
- * Grafts the two states a collection has that are not its content.
11
- *
12
- * **Empty and loading are different answers and a reader can tell them apart.**
13
- * "No results" under a spinner is a lie for as long as the request is in
14
- * flight, and a blank box is not an answer at all: a reader cannot know whether
15
- * a list is empty, still arriving, or broken.
16
- *
17
- * **`aria-busy` on the collection, not on the message.** A reader with a screen
18
- * reader is told the region is changing and then told once, politely, what it
19
- * settled on: a live region that fires on every intermediate state is a reader
20
- * being read a progress bar.
21
- *
22
- * **The status replaces the rows rather than sitting above them.** A list that
23
- * shows "Loading" over the last request's results is a list that lies about
24
- * which question it is answering, and this is the point in a component's life
25
- * when a consumer is most likely to be looking at stale content.
26
- *
27
- * **`empty` and `loading` are documented on each of the five families that
28
- * apply this, rather than here.** See `MixinField` for why a class-level tag on
29
- * a mixin reaches no manifest.
30
- */
31
9
  export const MixinCollection = (Base) => {
32
10
  class Collection extends Base {
33
11
  constructor() {
34
12
  super(...arguments);
35
- /**
36
- * What is said where there are none, and none are coming.
37
- */
38
13
  this.emptyText = 'Nothing to show';
39
- /**
40
- * Whether the rows are still on their way.
41
- *
42
- * Reflected, because a stylesheet needs it: the rows that are already there
43
- * are dimmed rather than removed while a new set arrives, and only CSS can
44
- * say that without the component knowing what a row looks like.
45
- */
46
14
  this.loading = false;
47
- /**
48
- * What is said while they are on their way.
49
- */
50
15
  this.loadingText = 'Loading';
51
- /**
52
- * Whether the rows have been looked at yet.
53
- */
54
16
  this.slotCounted = false;
55
- /**
56
- * How many there were, the last time anyone looked.
57
- *
58
- * **Never name this `rows`.** TypeScript's `private` is a compile-time
59
- * claim and the runtime field is whatever it is named, so a subclass
60
- * declaring `@property() rows` for the data a consumer hands it would share
61
- * one field with this counter: the slot count would overwrite the
62
- * consumer's array on every `updated`, and `hasRows` would read the array's
63
- * length as a slot count. Nothing would throw. `helix-data-table` takes
64
- * exactly that property.
65
- */
66
17
  this.slotRows = 0;
67
18
  }
68
- /**
69
- * Counts what a rows slot was given.
70
- *
71
- * Emptiness is a fact about light DOM children and the server has none to
72
- * count. `slotchange` never fires there, so `counted` is false exactly when
73
- * the answer is unknown, and an unknown answer is not "empty": a list that
74
- * shipped "Nothing to show" over markup about to have rows in it is the one
75
- * thing an empty state must never do.
76
- */
77
19
  countRows(event) {
78
20
  this.slotCounted = true;
79
21
  this.slotRows = event.target.assignedElements().length;
@@ -81,24 +23,6 @@ export const MixinCollection = (Base) => {
81
23
  get hasRows() {
82
24
  return this.slotCounted ? this.slotRows > 0 : true;
83
25
  }
84
- /**
85
- * Counts the rows slot after every render.
86
- *
87
- * `slotchange` alone is not enough, and the gap is exactly the case this
88
- * mixin exists for: the event fires when an assignment *changes*, and a
89
- * collection that arrives with nothing in it has no change to report. So a
90
- * list that was empty from the start never learned it was empty and said
91
- * nothing at all, which is the blank box a reader cannot tell from a broken
92
- * one. Measured on Table, whose rows slot starts empty.
93
- *
94
- * The default slot, by name, because that is where every collection in this
95
- * library puts its rows: Listbox its options, Tree its items, Table its
96
- * rows, Menu and Drilldown their pages. A named slot is a header or a
97
- * footer, and neither is content.
98
- *
99
- * `updated` never runs on the server, which is what keeps `counted` false
100
- * exactly when the answer is unknown.
101
- */
102
26
  updated(changed) {
103
27
  super.updated?.(changed);
104
28
  const slot = this.renderRoot?.querySelector?.('slot:not([name])');
@@ -111,13 +35,6 @@ export const MixinCollection = (Base) => {
111
35
  this.slotRows = rows;
112
36
  }
113
37
  }
114
- /**
115
- * The status line, or nothing where there is content to show.
116
- *
117
- * Loading wins over empty. A collection that has not answered yet is not
118
- * empty, and saying so is the most common version of this fault: the reader
119
- * is told there is nothing a moment before there is something.
120
- */
121
38
  renderStatus(hasContent) {
122
39
  if (this.loading) {
123
40
  return html `<p part="loading" role="status">${this.loadingText}</p>`;
@@ -8,57 +8,13 @@ import { property } from 'lit/decorators.js';
8
8
  import { HelixActivateEvent, HelixBeforeActivateEvent } from '../../events/events.js';
9
9
  import { requestChange } from '../../utils/request-change/request-change.js';
10
10
  import { resolveActiveItem, } from '../../utils/resolve-active-item/resolve-active-item.js';
11
- /**
12
- * Grafts "which one of these is the current one" onto a container.
13
- *
14
- * For a set where one member is current and the set is not a form control:
15
- * tabs, a tree, a nav bar, a carousel. Where it *is* a form control the value
16
- * is submitted and belongs to `MixinControllerValue`, which is why these are
17
- * two mixins and not one with a flag.
18
- *
19
- * **Announcing before applying is the part worth sharing.** Writing the current
20
- * value straight from the item's event leaves a consumer no way to refuse a
21
- * change, and whether a selection can be refused must not depend on which
22
- * component a reader is looking at.
23
- *
24
- * **The fallback is the other half.** `items.find(match) ?? items[0]` is not
25
- * the obvious line it looks like: it is what makes a set whose `active` names
26
- * nothing still show something, which is every set before its consumer has set
27
- * one and every set whose current item has just been removed. `resolveActive`
28
- * also skips disabled items in that fallback, or a set opening on a disabled
29
- * first item shows it as current.
30
- */
31
11
  export const MixinControllerActive = (Base) => {
32
- // JSDoc is duplicated from the interface deliberately. The interface is what
33
- // TypeScript consumers see; this class is what the manifest analyzer reads
34
- // when resolving inherited members into dependent packages.
35
12
  class ActiveElement extends Base {
36
13
  constructor() {
37
14
  super(...arguments);
38
- /**
39
- * Initial item for uncontrolled use.
40
- *
41
- * Ignored once `active` is written.
42
- */
43
15
  this.defaultActive = '';
44
- /**
45
- * The state, whoever set it, and `undefined` until somebody has.
46
- *
47
- * One field and not two. Writing always applies, the way a native control
48
- * does when a script assigns to it.
49
- */
50
16
  this.activeState = undefined;
51
17
  }
52
- /**
53
- * Which item is current, by its `value`.
54
- *
55
- * Not `option.active`, which is the option the keyboard is on: that is a
56
- * different question from which one is chosen.
57
- *
58
- * Writing always applies, so `<helix-tabs active="one">` starts on that
59
- * tab, and `default-active` is how markup says where to start without
60
- * naming the current one.
61
- */
62
18
  get active() {
63
19
  return this.activeState ?? this.defaultActive;
64
20
  }
@@ -67,16 +23,6 @@ export const MixinControllerActive = (Base) => {
67
23
  this.activeState = next;
68
24
  this.requestUpdate('active', previous);
69
25
  }
70
- /**
71
- * Requests a new current item, and reports whether the request stood.
72
- *
73
- * Announces before it acts, so `preventDefault()` on the `helix-before-activate` is
74
- * a refusal and nothing moves. Unlike a native control, nothing has moved
75
- * by the time this runs, so refusing is simply not writing: there is
76
- * nothing to put back. Nobody refusing means the set updates itself,
77
- * whether or not a consumer declared `active`, a declared property being a
78
- * seed and a correction.
79
- */
80
26
  requestActive(next) {
81
27
  if (next === this.active) {
82
28
  return true;
@@ -96,37 +42,9 @@ export const MixinControllerActive = (Base) => {
96
42
  }
97
43
  return applied;
98
44
  }
99
- /**
100
- * Says the current child moved, once it has.
101
- *
102
- * `helix-activate` rather than a native word: nothing on the platform means
103
- * "a set chose a different member", and borrowing `change` would be a lie
104
- * about what moved.
105
- *
106
- * A consumer who only wants to know which child is current cannot use
107
- * `helix-before-activate` for it, that one firing *before* `apply`, so
108
- * `currentTarget.active` is the previous value inside the handler and
109
- * `detail.value` is a proposal another listener may refuse. `helix-select`
110
- * is no substitute either: it fires when an item is *chosen* and not when
111
- * `active` is assigned.
112
- *
113
- * **After `apply`, which is the whole reason it is a second event**, so the
114
- * state is on the element by the time a handler reads it. Not cancelable:
115
- * the decision was taken by the event before this one, and two refusable
116
- * events for one transition are two answers to one question.
117
- */
118
45
  announceActivated(value) {
119
46
  this.dispatchEvent(new HelixActivateEvent({ value }));
120
47
  }
121
- /**
122
- * Which item is current, given what there is to choose from.
123
- *
124
- * The first enabled one when `active` names nothing, which is a set before
125
- * its consumer has chosen and a set whose current item has just been
126
- * removed. Disabled items are skipped in the fallback but not in the match:
127
- * a consumer who names a disabled item meant it, and quietly moving to
128
- * another would be the component overruling them.
129
- */
130
48
  resolveActive(items) {
131
49
  return resolveActiveItem(this.active, items);
132
50
  }
@@ -7,47 +7,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { property } from 'lit/decorators.js';
8
8
  import { HelixBeforeToggleEvent } from '../../events/events.js';
9
9
  import { requestChange } from '../../utils/request-change/request-change.js';
10
- /**
11
- * Grafts checked state onto a base class.
12
- *
13
- * `checked` is a seed and a correction rather than a claim: the element applies
14
- * its own changes from wherever markup started it, `defaultChecked` is what a
15
- * reset returns to, and a consumer takes the decision by refusing the
16
- * `helix-before-toggle`.
17
- *
18
- * Records `:state(checked)` whenever the element is checked. Styled tiers
19
- * select on that rather than on `[checked]`, which is absent until somebody
20
- * writes it.
21
- */
22
10
  export const MixinControllerChecked = (Base) => {
23
- // JSDoc is duplicated from MixinControllerCheckedInterface deliberately. The
24
- // interface is what TypeScript consumers see; this class is what the manifest
25
- // analyzer reads when resolving inherited members into dependent packages.
26
11
  class CheckedElement extends Base {
27
12
  constructor() {
28
13
  super(...arguments);
29
- /**
30
- * Initial checked state for uncontrolled use, where nothing declared one.
31
- */
32
14
  this.defaultChecked = false;
33
- /**
34
- * The state, whoever set it, and `undefined` until somebody has.
35
- *
36
- * One field and not two. Writing always applies, the way a native control
37
- * does when a script assigns to it.
38
- */
39
15
  this.checkedState = undefined;
40
16
  }
41
- /**
42
- * Whether the control is checked.
43
- *
44
- * One property rather than two. Writing always applies;
45
- * `<helix-checkbox checked>` is the same statement in markup.
46
- *
47
- * Not reflected: the attribute is the consumer's input and stays as they
48
- * wrote it, and `:state(checked)` carries the resolved state for
49
- * stylesheets.
50
- */
51
17
  get checked() {
52
18
  return this.checkedState ?? this.defaultChecked;
53
19
  }
@@ -56,33 +22,10 @@ export const MixinControllerChecked = (Base) => {
56
22
  this.checkedState = next;
57
23
  this.requestUpdate('checked', previous);
58
24
  }
59
- /**
60
- * Records the resolved state as the custom state `checked`.
61
- *
62
- * **A custom state and never `data-checked`.** An attribute written here
63
- * means `<helix-checkbox default-checked>` comes back from a consumer's
64
- * server without it and gains it the moment the definition lands, which
65
- * React lists as a hydration mismatch and does not patch up.
66
- * `HelixElement.toggleState` says why the host is the wrong place to keep a
67
- * component's own bookkeeping.
68
- *
69
- * In `willUpdate`, which is where the resolved state is first known and is
70
- * what a subclass rendering from it expects.
71
- */
72
25
  willUpdate(changed) {
73
26
  super.willUpdate(changed);
74
27
  this.toggleState('checked', this.checked);
75
28
  }
76
- /**
77
- * Requests a checked change, and reports whether it was applied.
78
- *
79
- * Announces before it acts, so `preventDefault()` on the
80
- * `helix-before-toggle` is a refusal and nothing moves. Nobody refusing
81
- * means the element updates itself, whether or not a consumer declared the
82
- * property: a declared property is a seed and a correction, the way
83
- * `<input value>` is, and a consumer who wants the change for themselves
84
- * refuses it and assigns their own.
85
- */
86
29
  requestChecked(next) {
87
30
  if (this.checked === next) {
88
31
  return true;
@@ -98,37 +41,16 @@ export const MixinControllerChecked = (Base) => {
98
41
  next,
99
42
  });
100
43
  if (applied) {
101
- /* `input` here and `change` relayed from the inner control, which is
102
- the pair a native checkbox sends. Announced after `apply`, so a
103
- handler reading `currentTarget.checked` reads the new state. */
104
44
  this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
105
45
  }
106
46
  return applied;
107
47
  }
108
- /** Requests the opposite of the current state. */
109
48
  toggleChecked() {
110
49
  return this.requestChecked(!this.checked);
111
50
  }
112
- /**
113
- * Restores the control when the browser brings a form back.
114
- *
115
- * The mixin owns `checked`, so the mixin owns putting it back: written on
116
- * one consumer and not the other, a reader who navigates away and comes
117
- * back to a restored form finds that control flipped off.
118
- *
119
- * Through the public accessor rather than the private field, so a restore
120
- * takes the same path a script assignment does.
121
- */
122
51
  formStateRestoreCallback(state) {
123
52
  this.checked = state !== null;
124
53
  }
125
- /**
126
- * Drops the recorded state, so the control reads `defaultChecked` again.
127
- *
128
- * A form reset has to return a control to what its markup declared, and no
129
- * assignment to `checked` can do that: any assignment records a state. The
130
- * recorded state lives here, so this is where it is dropped.
131
- */
132
54
  resetChecked() {
133
55
  const previous = this.checked;
134
56
  this.checkedState = undefined;