@fusion.dev/helix-headless 0.1.0 → 0.3.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 (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -7,122 +7,37 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { AriaForwardController, customElement, HelixFormElement, MixinControllerChecked, MixinField, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A box that is ticked, or not, or neither.
12
- *
13
- * @description
14
- * Renders a real `<input type="checkbox">`, so the space key, the focus ring,
15
- * and the indeterminate glyph come from the platform. `delegatesFocus` puts
16
- * focus on it, and it serializes into the server's markup, so the control is
17
- * reachable before any script loads.
18
- *
19
- * **The input inside a shadow root submits nothing**, because the form is on
20
- * the other side of the boundary. The host is form-associated instead and
21
- * reports its own value through `ElementInternals`, which is also what carries
22
- * validity and what answers a form reset.
23
- *
24
- * A checked box submits its `value`, and an unchecked one submits nothing at
25
- * all. That is what a native checkbox does, and a form built around one reads
26
- * the absence rather than a `false`.
27
- *
28
- * `indeterminate` is presentation and nothing else. It is neither checked nor
29
- * unchecked to a reader, it never submits, and the first click resolves it: a
30
- * third state that survives interaction is a control nobody can finish.
31
- *
32
- * @since 0.1.0
33
- * @status experimental
34
- * @category forms
35
- * @aliases Tickbox, Toggle
36
- * @documentation https://helix.fusion.dev/components/checkbox
37
- *
38
- * @slot - The label.
39
- *
40
- * @event {Event} change - Relayed from the input once the state has changed.
41
- * @event {HelixBeforeToggleEvent} helix-before-toggle - Fired before the state changes, and cancelable.
42
- * @event {Event} input - Announced once the state has moved.
43
- *
44
- * @csspart box - The frame the input is painted as, and the mark sits over.
45
- * @csspart control - The input.
46
- * @csspart feedback - What is wrong with it.
47
- * @csspart field - The row the box and its label share, and the surface the card appearance draws.
48
- * @csspart hint - Help under the control, while nothing is wrong.
49
- * @csspart label - The label beside it.
50
- * @csspart mark - The tick, or the dash when the box is indeterminate.
51
- */
52
10
  let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerChecked(MixinField(HelixFormElement)) {
53
11
  constructor() {
54
12
  super(...arguments);
55
- /* A consumer's `aria-describedby` names ids in their own tree and the control
56
- is in this shadow root, so the reference has to be carried down as an
57
- element rather than copied as an id. See `AriaForwardController`: measured
58
- in Chrome's own tree, the host held the description and the control the
59
- reader is actually on was exposed without it. */
60
13
  this.aria = new AriaForwardController(this, {
61
14
  target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
62
15
  });
63
- /**
64
- * Whether the box shows as neither checked nor unchecked.
65
- *
66
- * Presentation only. It never submits, and the first change resolves it.
67
- */
68
16
  this.indeterminate = false;
69
- /**
70
- * Value submitted when the box is checked.
71
- */
72
17
  this.value = 'on';
73
18
  this.handleChange = (event) => {
74
19
  const input = event.target;
75
- // The input has already flipped itself. Where the change is refused it is
76
- // put back to whatever the element resolves to, so the box never shows a
77
- // state the element does not hold.
78
20
  const next = input.checked;
79
- /* The dash goes only where the change is taken. Clearing it
80
- * unconditionally leaves a refused change holding the checked state and
81
- * losing the third one: a box neither ticked nor dashed, which is a state
82
- * nobody asked for. Refusing leaves everything as it was, which is the
83
- * argument the line below already makes for `checked`. */
84
21
  if (this.requestChecked(next)) {
85
22
  this.indeterminate = false;
86
23
  }
87
24
  input.checked = this.checked;
88
25
  this.relayNativeEvent(event);
89
26
  };
90
- /**
91
- * Whether there is a label to leave room for, written to the host.
92
- *
93
- * The row is a flex line with a gap in it, so a label span rendered empty is
94
- * still a box and still takes that gap: an unlabelled box came out with eight
95
- * pixels of nothing after it. The input answers this by not rendering the
96
- * element, which cannot work here, because a checkbox is named by its own
97
- * content and the property is only that slot's fallback. Gating on the
98
- * property would unlabel every `<helix-checkbox>Text</helix-checkbox>` ever
99
- * written.
100
- *
101
- * So the slot is asked instead, the same way `Tooltip` asks its own: with
102
- * `flatten`, because here the fallback counts, being where the property
103
- * renders. Client only, so a server-rendered unlabelled checkbox carries the
104
- * gap for one frame. That is a control with no accessible name, which has a
105
- * worse problem than its spacing.
106
- */
107
27
  this.handleLabelChange = () => {
108
28
  this.syncLabelPresence();
109
29
  };
110
30
  }
111
- /** Focus is delegated to the input, so the host is never the focus target. */
112
31
  static { this.shadowRootOptions = {
113
32
  ...HelixFormElement.shadowRootOptions,
114
33
  delegatesFocus: true,
115
34
  }; }
116
- /** Reports the value and the validity the host stands behind. */
117
35
  sync() {
118
36
  this.internals.setFormValue(this.checked ? this.value : null);
119
37
  this.reportRequired(!this.checked, 'Tick this box to continue.', 'control');
120
38
  }
121
- /** Returns the box to the state its markup declared. */
122
39
  formResetCallback() {
123
40
  this.indeterminate = false;
124
- // `resetChecked` drops both halves, the consumer's and the element's, so
125
- // there is nothing to clear before it.
126
41
  this.resetChecked();
127
42
  }
128
43
  willUpdate(changed) {
@@ -134,41 +49,13 @@ let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerC
134
49
  this.sync();
135
50
  this.syncLabelPresence();
136
51
  }
137
- /**
138
- * Asks the slot, and records the answer as a state.
139
- *
140
- * Called on `slotchange` and after every update, and the second is not
141
- * belt-and-braces: **`slotchange` does not fire when nothing is assigned.**
142
- * Without it a checkbox labeled only by its property, which is most of them,
143
- * never hears about its own label and renders without one.
144
- *
145
- * **`:state(has-label)` and not `data-has-label`.** The answer comes from
146
- * `assignedNodes` and a server has no tree to ask, so an attribute would
147
- * appear on every labeled checkbox the instant it upgraded and on none that
148
- * any server sent. See `HelixElement.toggleState`.
149
- */
150
52
  syncLabelPresence() {
151
53
  const slot = this.renderRoot.querySelector('[part="label"] slot');
152
54
  const filled = (slot?.assignedNodes({ flatten: true }) ?? []).some((node) => node.textContent?.trim() !== '' || node.nodeType === Node.ELEMENT_NODE);
153
55
  this.toggleState('has-label', filled);
154
- /* And `hidden` on the region itself, because the state is on the host and
155
- this tier has no sheet to turn one into the other, so a checkbox with no
156
- label would keep an empty label box beside it. The styled rule stays,
157
- since it hides through a descendant selector this cannot replace. */
158
56
  this.renderRoot.querySelector('[part="label"]')?.toggleAttribute('hidden', !filled);
159
57
  }
160
58
  render() {
161
- /* A real `<label>` around both, which is the whole of what makes the text a
162
- target. A `<span>` beside the input leaves pressing the words doing
163
- nothing.
164
-
165
- Labeling is behavior, which is what earns an element here rather than a
166
- bare slot: the wrapper is not decoration and there is no other way to get
167
- it. The browser handles the forwarding, so there is no click handler to
168
- write and none to get wrong.
169
-
170
- The one line of description goes under that row, which is where a field
171
- puts it too. */
172
59
  return html `<label part="field">
173
60
  <span part="box">
174
61
  <input
@@ -8,99 +8,21 @@ import { customElement, DisabledCascadeController, HelixBeforeChangeEvent, Helix
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessCheckbox } from '../../checkbox/root/checkbox-root.js';
11
- /**
12
- * @summary Groups related checkboxes, and reports them as one set.
13
- *
14
- * @description
15
- * The host is the group, so `role="group"` sits on it through internals and
16
- * `label` names it. A run of checkboxes with no name is announced as separate
17
- * controls, which is what the grouping exists to say it is not.
18
- *
19
- * **Each checkbox still submits itself.** They are form-associated already and
20
- * a native group is several inputs sharing a name, so this adds no form value
21
- * of its own: it would submit the same answers twice. What it adds is the
22
- * grouping, a shared `name` so it is written once, and one place to read and
23
- * write the whole set.
24
- *
25
- * `value` is the checked values, comma separated, in the order the boxes are
26
- * written. Setting it ticks exactly those and unticks the rest.
27
- *
28
- * **Its message is always given, never derived.** The group is form-associated
29
- * for validity alone and never calls `setFormValue`, so `feedback` is what shows
30
- * here: the browser has nothing to say about a set it does not submit.
31
- *
32
- * @since 0.1.0
33
- * @status stable
34
- * @category forms
35
- * @aliases Checkbox list, Multi-select
36
- * @documentation https://helix.fusion.dev/components/checkbox-group
37
- *
38
- * @slot - The checkboxes.
39
- * @slot hint - Richer than a string.
40
- * @slot label - Richer than a string, where the label is more than one.
41
- *
42
- * @event {Event} change - Announced once the value has moved.
43
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before any box changes, with the whole set. Cancelable: refusing it takes the decision and nothing moves.
44
- * @event {Event} input - Announced once the value has moved.
45
- *
46
- * @csspart feedback - What is wrong with it.
47
- * @csspart hint - Help under the set.
48
- * @csspart items - The list itself, which is what `orientation` lays out.
49
- * @csspart label - The group's name, over the set.
50
- */
51
11
  let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinField(HelixFormElement) {
52
12
  constructor() {
53
- /* `name` is inherited from `HelixFormElement`, reflected and with the
54
- converter that makes an empty one absent rather than `name=""`. What it
55
- means differs slightly and the property does not: the group submits nothing
56
- under it, and hands it to every box that has none. */
57
13
  super(...arguments);
58
- /**
59
- * The checked values, comma separated.
60
- */
61
14
  this.value = '';
62
- /* Stated once, and the group never thinks about it again. The controller is
63
- what stops the cascade being hand-called from whichever methods a component
64
- happens to have. */
65
15
  this.cascade = new DisabledCascadeController(this, {
66
16
  items: () => this.boxes,
67
17
  });
68
- /**
69
- * Reads a box's requested change into `value`, and says so once.
70
- *
71
- * The box is found from the event's target, which does two things. It tells
72
- * the group which box asked, where the detail carries only the shared name.
73
- * And it ignores the group's own dispatch, which lands on this same listener
74
- * because the group is what it is dispatched from: without the guard the
75
- * group answers itself until the stack gives out.
76
- *
77
- * The requested state comes from the event rather than from the box. Driving
78
- * the boxes from `value` makes them controlled, so a controlled box applies
79
- * nothing on its own and reports what it was asked for. Reading `checked`
80
- * here would read the state it still holds and nothing would ever change.
81
- */
82
18
  this.handleToggle = (event) => {
83
19
  const target = event.target;
84
20
  const changed = this.boxes.find((box) => box === target);
85
21
  if (!changed) {
86
22
  return;
87
23
  }
88
- /* A box's `helix-before-toggle` bubbles, and the group is the single voice
89
- for the set: a consumer listening here wants the whole answer, not one
90
- box's half of it. */
91
24
  event.stopImmediatePropagation();
92
25
  const wanted = event.detail.checked;
93
- /* Announced before it is written, never after: dispatching once the group's
94
- value has moved leaves `preventDefault()` refusing a set that has already
95
- changed.
96
-
97
- Hand-rolled rather than through `MixinControllerValue`, and that is a
98
- decision rather than an omission. The group's `value` is a derived read of
99
- the boxes rather than state it owns, so there is nothing for the mixin to
100
- control.
101
-
102
- The boxes have already moved either way. Each answered the reader before
103
- this ran, so refusing here stops only the group's value from following. */
104
26
  const next = this.boxes
105
27
  .filter((box) => (box === changed ? wanted : box.checked))
106
28
  .map((box) => box.value)
@@ -109,50 +31,18 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
109
31
  return;
110
32
  }
111
33
  this.value = next;
112
- /* Under the platform's own name, after the value has moved, so a consumer
113
- binding `onChange` reads the group's new value off the target.
114
-
115
- `input` beside it, because this is a discrete control and that is the
116
- pair a native `<select>` sends and the pair every other discrete control
117
- here sends through `MixinControllerValue`. */
118
34
  this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
119
35
  this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
120
36
  };
121
- /**
122
- * Stops a box's own announcement, for the reason the request above is stopped.
123
- *
124
- * A box announces `input` and `change` from its own host, which is what makes
125
- * `onChange` work on a checkbox, and both bubble up through this group:
126
- * measured, one click produced `change` from the group and `input` and
127
- * `change` from the box, all three at a consumer listening here, two of them
128
- * describing one box rather than the set.
129
- *
130
- * `stopImmediatePropagation` and not `stopPropagation`: the box's event and
131
- * this group's own carry one name from what a consumer sees as one element,
132
- * so stopping propagation alone still leaves every other listener on this
133
- * host receiving both.
134
- *
135
- * A listener on the box itself is unaffected, which is where per-box handling
136
- * belongs. Each box is form-associated and submits itself; what this stops is
137
- * one box answering a question asked of the set.
138
- */
139
37
  this.handleMemberNative = (event) => {
140
38
  if (event.target !== this) {
141
39
  event.stopImmediatePropagation();
142
40
  }
143
41
  };
144
42
  }
145
- /** The boxes, in light-DOM order. Client only: the server has no tree. */
146
43
  get boxes() {
147
44
  return [...this.children].filter((child) => child instanceof HelixHeadlessCheckbox);
148
45
  }
149
- /**
150
- * Pushes the group's name and value onto the boxes.
151
- *
152
- * As properties rather than attributes. A `name` written onto a box is a
153
- * change to markup the consumer rendered, which a React consumer's hydration
154
- * reports and does not patch up. A box that names itself keeps its own.
155
- */
156
46
  sync() {
157
47
  const wanted = new Set(this.value
158
48
  .split(',')
@@ -165,21 +55,6 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
165
55
  box.checked = wanted.has(box.value);
166
56
  }
167
57
  }
168
- /**
169
- * Reads the boxes' own state into `value`, once, before anything is pushed.
170
- *
171
- * Seeding, and never a guard in `sync` refusing to write while `value` is
172
- * empty. The guard would answer the same case, a group written with no `value`
173
- * whose boxes declare their own `checked` being cleared on the first update,
174
- * and it also makes `group.value = ''` do nothing at all: the obvious way to
175
- * clear a group would silently not, and the last box could not be unticked
176
- * through the group either.
177
- *
178
- * The markup decides the starting set, and from then on `value` is what the
179
- * boxes follow, including when it is empty. The group's `value` is a derived
180
- * read of the boxes rather than state it owns, so reading it out of them at
181
- * the start is what it already means.
182
- */
183
58
  seed() {
184
59
  const checked = this.boxes.filter((box) => box.checked).map((box) => box.value);
185
60
  if (checked.length > 0) {
@@ -200,39 +75,15 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
200
75
  }
201
76
  willUpdate(changed) {
202
77
  super.willUpdate(changed);
203
- /* The string half, which is what a server can write. `linkHost` adds the
204
- element references on the client, and those win where a consumer slotted
205
- a label richer than a string. */
206
78
  hostAria(this.internals, { label: this.label, role: 'group' });
207
79
  this.internals.ariaRequired = this.required ? 'true' : null;
208
80
  }
209
81
  firstUpdated(changed) {
210
82
  super.firstUpdated(changed);
211
- /* Before the first `sync`, and only ever once: after this the group's value
212
- is what the boxes follow. Client only, the server having no boxes to
213
- read. */
214
83
  if (this.value === '') {
215
84
  this.seed();
216
85
  }
217
86
  }
218
- /**
219
- * Refuses an empty set where one was required.
220
- *
221
- * **The group is form-associated, and submits nothing.** Nothing here calls
222
- * `setFormValue`, so the set that reaches the server is exactly the boxes,
223
- * each of which submits itself. What form association buys is the other half
224
- * of participation, validity, and there is no other mechanism for it:
225
- * `setValidity` throws `NotSupportedError` on an element the platform does not
226
- * know is a control. happy-dom's shim does not throw, so a spec passes against
227
- * code that fails on every update in Chromium.
228
- *
229
- * It is also what makes the rest work: the form refuses to submit, which fires
230
- * `invalid`, which is what marks the field touched and puts the message on
231
- * screen.
232
- *
233
- * Anchored on the list, because the browser needs somewhere to point the
234
- * bubble.
235
- */
236
87
  validate() {
237
88
  this.reportRequired(this.value === '', 'Choose at least one to continue.', 'items');
238
89
  }
@@ -243,16 +94,9 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
243
94
  this.linkHost();
244
95
  }
245
96
  render() {
246
- /* The list is an element of its own, because the label and the hint are the
247
- host's children too and `orientation` lays out whatever is in the flex
248
- container. On the host, a horizontal group would have put its own label
249
- in the row beside the first box. */
250
97
  return html `${this.renderGroupLabel()}
251
98
  <div part="items">
252
- ${
253
- /* A box arriving after first render has neither the shared name nor
254
- the value's checked state until something pushes them onto it. */
255
- ''}
99
+ ${''}
256
100
  <slot @slotchange=${() => this.requestUpdate()}></slot>
257
101
  </div>
258
102
  ${this.renderHint()}${this.renderFeedback()}`;
@@ -6,32 +6,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
- /**
10
- * @summary A fragment of code inside a line of prose.
11
- *
12
- * @description
13
- * **A real `<code>`, because the element is the meaning.** A styled span reads
14
- * as code and is not: a screen reader announces nothing about it, a translation
15
- * tool will happily translate it, and a search that understands documents
16
- * cannot tell it from emphasis. The tag is the whole point, which is also why
17
- * this component is so small.
18
- *
19
- * **Inline, and only inline.** A block of code is `CodeBlock`, a separate
20
- * family rather than a variant here. The two differ in more than presentation:
21
- * one flows with a sentence and must never scroll, the other is a figure with
22
- * its own box, its own overflow and something to copy. Namespacing them
23
- * together would say `CodeBlock` is part of `Code`, and it is not.
24
- *
25
- * @since 0.1.0
26
- * @status stable
27
- * @category typography
28
- * @aliases Inline code, Kbd
29
- * @documentation https://helix.fusion.dev/components/code
30
- *
31
- * @slot - The code.
32
- *
33
- * @csspart code - The `<code>` element.
34
- */
35
9
  let HelixHeadlessCode = class HelixHeadlessCode extends HelixElement {
36
10
  render() {
37
11
  return html `<code part="code"><slot></slot></code>`;
@@ -9,126 +9,26 @@ import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import '../../copy-button/index.js';
12
- /**
13
- * @summary A block of code, with somewhere to put its name and a way to take it.
14
- *
15
- * @description
16
- * **`<pre><code>` and nothing cleverer.** The pair is what the platform means
17
- * by a block of code: `pre` keeps the whitespace, `code` says what the
18
- * whitespace is for, and losing either one loses something no styling puts
19
- * back. Indentation in a `div` is a paragraph with odd spacing.
20
- *
21
- * **It scrolls rather than wraps.** A wrapped line of code is a lie about where
22
- * the line ends, and in a language where a line break can be significant it is
23
- * a lie that costs someone an afternoon. The box takes its own scrollbar.
24
- *
25
- * **The copy button is here because the alternative is selecting it by hand.**
26
- * Every consumer would otherwise put one there, slightly differently, and the
27
- * one that forgot would be the one with the longest snippet. It is on unless
28
- * `no-copy` says otherwise, and takes its text from the slot rather than from a
29
- * property, so there is no second copy of the code to keep in step.
30
- *
31
- * A `Code` for a fragment inside a sentence. Separate families, because one
32
- * flows with prose and the other is a figure.
33
- *
34
- * @since 0.1.0
35
- * @status stable
36
- * @category typography
37
- * @aliases Code block, Snippet, Pre
38
- * @documentation https://helix.fusion.dev/components/code-block
39
- *
40
- * @slot - The code.
41
- *
42
- * @csspart block - The scrolling `<pre>`.
43
- * @csspart code - The `<code>` inside it.
44
- * @csspart copy - The copy button.
45
- * @csspart copy-button - The inner button.
46
- * @csspart copy-mark - The clipboard glyph, and the confirmation that replaces it.
47
- * @csspart copy-tooltip - The bubble, forwarded from the tooltip inside.
48
- * @csspart copy-tooltip-arrow - The point, resting against the trigger.
49
- * @csspart frame - The code and the copy button beside it.
50
- * @csspart header - The bar carrying the name, where there is one.
51
- * @csspart label - The file name or language, where one is given.
52
- */
53
12
  let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
54
13
  constructor() {
55
14
  super(...arguments);
56
- /**
57
- * What to call this block, usually a file name or a language.
58
- */
59
15
  this.label = '';
60
- /**
61
- * Whether to leave the copy button off.
62
- *
63
- * Negative, which is not a style choice. A boolean attribute is true whenever
64
- * it is *present*, so a `copyable` defaulting to true could never be turned
65
- * off from markup: `copyable="false"` is the string "false", which is
66
- * present, which is true. Measured, and it is why this is named the other way
67
- * round. The platform already writes booleans like this for the same reason,
68
- * `novalidate` and `nomodule` and `noreferrer` among them.
69
- *
70
- * **Reflected, because the gutter is a stylesheet rule.** `code-block.css`
71
- * reserves the copy button's width with `:host(:not([no-copy]))`, so the
72
- * attribute is what decides whether the block pays for a control. Unreflected,
73
- * setting it from script takes the button away (`render` reads the property)
74
- * and leaves the 40px it stood in reserved.
75
- *
76
- * A false boolean reflects as the attribute's absence, so this costs no
77
- * `no-copy=""` on hosts that never asked for one, and the rule about a
78
- * reflected property taking no default is satisfied by the default being
79
- * `false`.
80
- */
81
16
  this.noCopy = false;
82
17
  }
83
- /**
84
- * The code as text, read from the slot rather than stored.
85
- *
86
- * Client only, and it has to be: the server has no children to read. The
87
- * copy button is a client-side affordance anyway, so nothing is lost, and
88
- * taking the text from a property instead would be a second copy of the code
89
- * that goes stale the moment the slot changes.
90
- */
91
18
  get text() {
92
19
  const slot = this.renderRoot?.querySelector('slot');
93
20
  return (slot?.assignedNodes() ?? []).map((node) => node.textContent ?? '').join('');
94
21
  }
95
- /**
96
- * One more render, once there is a slot to read.
97
- *
98
- * The first render has no shadow children yet, so `text` is empty and the
99
- * copy button is bound to nothing. `slotchange` normally schedules the render
100
- * that fixes it, and leaning on that alone was measured to leave the button
101
- * empty through four update cycles: the event is not guaranteed to have
102
- * arrived, and in one environment never arrives at all. A component should not
103
- * need an event to show its own content, so it asks for the second render
104
- * itself and `slotchange` is left to handle the case it is actually for, which
105
- * is content changing later.
106
- */
107
22
  firstUpdated() {
108
23
  this.requestUpdate();
109
24
  }
110
25
  render() {
111
- /* The copy button sits beside the code, not in a bar above it. In the
112
- header, a block with nothing to call itself still draws a full-width bar
113
- to hold one button, so a `Usage` section of one-line imports is a run of
114
- empty strips. Beside the code, the header exists only when there is a
115
- name to put in it. */
116
26
  return html `${this.label ? html `<div part="header"><span part="label">${this.label}</span></div>` : nothing}
117
27
  <div part="frame">
118
28
  <pre part="block"><code part="code"><slot @slotchange=${() => this.requestUpdate()}></slot></code></pre>
119
29
  ${this.noCopy ? nothing : this.renderCopyButton()}
120
30
  </div>`;
121
31
  }
122
- /**
123
- * The button that takes the code, behind a seam the styled tier can move.
124
- *
125
- * A method rather than a fragment inline, so a tier above can render its own
126
- * copy control, wrap this one, or pass it something the base does not know
127
- * about, without reimplementing the whole template to do it. The tag is
128
- * interpolated rather than written, so `tier` resolves it: this renders the
129
- * base button, and a styled subclass that sets `tier` renders the styled one
130
- * from the same template.
131
- */
132
32
  renderCopyButton() {
133
33
  return html `<${this.tag('copy-button')}
134
34
  part="copy"