@fusion.dev/helix-headless 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 (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,79 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { anchorElement, CommandController, customElement, deepActiveElement, HelixElement, MixinControllerOpen, PopoverController, tellDisclosure, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A panel that opens against a trigger and holds anything.
12
- *
13
- * @description
14
- * The general case the specific overlays are versions of. `Menu` holds
15
- * commands, `Select` holds options, `Tooltip` holds a description; this holds
16
- * whatever the consumer puts in it, which is why it has no keyboard of its own
17
- * and no opinion about what is inside.
18
- *
19
- * **The trigger is slotted and stays the consumer's own element.** Nothing is
20
- * written onto it: no `aria-expanded`, no `id`, no `tabindex`. A component that
21
- * decorates a consumer's markup after upgrading is a component whose output a
22
- * React consumer's hydration disagrees with, and the attribute they would care
23
- * about most is the one that changes on every open.
24
- *
25
- * **The panel is in the top layer**, through the popover API, so it clears any
26
- * ancestor with `overflow: hidden` without a `z-index` fight. `position: fixed`
27
- * does not do that: it is still clipped by `overflow`, and re-anchored entirely
28
- * by an ancestor carrying `transform`, `filter` or `contain`.
29
- *
30
- * **It opens on click, and from a command.** `command="--show"` and its
31
- * siblings work here the way they do on `Dialog` and `Drawer`, so a trigger
32
- * elsewhere on the page can open it without a line of script and without being
33
- * slotted into it.
34
- *
35
- * @since 0.1.0
36
- * @status stable
37
- * @category overlays
38
- * @aliases Popper, Flyout, Overlay
39
- * @documentation https://helix.fusion.dev/components/popover
40
- *
41
- * @slot - The contents of the panel.
42
- * @slot trigger - What opens it.
43
- *
44
- * @event {HelixBeforeCloseEvent} helix-before-close - Fired before it closes, and cancelable.
45
- * @event {HelixBeforeOpenEvent} helix-before-open - Fired before it opens, and cancelable.
46
- * @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
47
- *
48
- * @csspart content - The scrolling region inside the panel.
49
- * @csspart popover - The panel.
50
- */
51
10
  let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpen(HelixElement) {
52
11
  constructor() {
53
12
  super(...arguments);
54
- /**
55
- * Accessible name for the panel.
56
- */
57
13
  this.label = 'Popover';
58
- /**
59
- * Space between the trigger and the panel, in pixels.
60
- */
61
14
  this.offset = 4;
62
- /**
63
- * Preferred side.
64
- *
65
- * It moves if that side does not fit.
66
- */
67
15
  this.placement = 'bottom-start';
68
- /* The panel, plus everything that has to happen around it.
69
- *
70
- * Called `panel` rather than `popover`, because `HTMLElement.popover` is the
71
- * popover API's own IDL attribute and a field of that name shadows it with
72
- * something of a different type. TypeScript objects; what it would have done
73
- * at runtime is break the API this is built on. */
74
16
  this.panel = new PopoverController(this, {
75
17
  anchor: () => this.trigger,
76
- /* The host *and* whatever invoked it, never the panel alone. An outside
77
- press is measured against this, and the styled host is
78
- `display: contents` with no box of its own, so a boundary of the panel
79
- alone would close on the very press that opened it. A `--toggle` button
80
- outside the host is part of the control by any reading a reader would
81
- give it: pressing it while the panel is open should close it once, not
82
- close it on `pointerdown` and reopen it on the `click` that follows. */
83
18
  boundary: () => [this, this.invoker],
84
19
  offset: () => this.offset,
85
20
  onDismiss: () => this.hide(),
@@ -113,72 +48,34 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
113
48
  }
114
49
  };
115
50
  }
116
- /** The element the panel opens against. */
117
51
  get trigger() {
118
- /* The invoker first, where there is one: a command names the element the
119
- reader actually pressed, and it is a better anchor than a slotted trigger
120
- the consumer may not have provided at all. */
121
52
  return (this.invoker ??
122
53
  anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this));
123
54
  }
124
- /** Opens the panel. */
125
55
  show() {
126
56
  if (this.open || !this.requestOpen(true)) {
127
57
  return;
128
58
  }
129
- // The raising happens in `updated()`, which `requestOpen` has just
130
- // scheduled: one path for both the imperative and the declarative case.
131
59
  }
132
- /** Closes the panel and puts focus back on the trigger. */
133
60
  hide() {
134
61
  if (!this.open || !this.requestOpen(false)) {
135
62
  return;
136
63
  }
137
64
  this.panel.hide();
138
- /* Only when focus is inside the panel that is going away. A popover
139
- dismissed by a press somewhere else has not taken focus with it, and
140
- pulling focus back to the trigger would move a reader who was already
141
- somewhere else on purpose. */
142
- /* `deepActiveElement`, which walks into shadow roots. `document.activeElement`
143
- stops at the outermost host, so a popover living inside another
144
- component's shadow root never saw its own focus and never gave it back to
145
- the trigger. The util exists for this. */
146
65
  if (this.contains(deepActiveElement())) {
147
66
  this.trigger.focus();
148
67
  }
149
68
  }
150
- /** Tells a slotted trigger the disclosure state, by property. See `tellDisclosure`. */
151
69
  tellTrigger() {
152
70
  const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
153
71
  tellDisclosure(slot?.assignedElements({ flatten: false })[0], this.open);
154
72
  }
155
- /**
156
- * Puts the panel where `open` says it should be, however it got there.
157
- *
158
- * Driven from the resolved state rather than from `show()` and `hide()`,
159
- * which is what makes the imperative and the declarative paths one path. Left
160
- * to the two methods, a panel opened by *declaring* `open` (or `default-open`,
161
- * or a consumer assigning the property) renders visible while never reaching
162
- * the top layer and never being anchored: clipped by the first ancestor with
163
- * `overflow`, and positioned wherever normal flow left it. The controller
164
- * ignores a `show()` on something already raised, so this costs nothing when
165
- * the methods did the work.
166
- */
167
73
  updated(changed) {
168
74
  super.updated(changed);
169
- /* Every update, not only the ones that move `open`: a trigger slotted after
170
- the first render has to be told too, and a slot change is not a property
171
- change. Idempotent, so saying it twice costs an assignment Lit compares
172
- and discards. */
173
75
  this.tellTrigger();
174
76
  if (!changed.has('open')) {
175
77
  return;
176
78
  }
177
- /* Raised here rather than a microtask later, and `updated()` is what makes
178
- that safe: the render has already been written, so the panel is no longer
179
- `display: none` and `showPopover` will not throw on it. Anything raising
180
- it before the render has to wait for `updateComplete`; from here there is
181
- nothing left to wait for. */
182
79
  if (this.open) {
183
80
  this.panel.show();
184
81
  }
@@ -7,139 +7,25 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary Shows how far along a task is, when that is known.
12
- *
13
- * @description
14
- * Determinate by default, and `indeterminate` for a task whose total is not
15
- * known yet. `Loader` is still the right answer where the wait is short or has
16
- * no place of its own on the page; a bar earns its width where the work is the
17
- * subject of the region it sits in.
18
- *
19
- * A bar that fills at a made-up rate would be a bar that lies, so the
20
- * indeterminate one does not fill: it runs a segment across the track, which
21
- * says work is happening and claims no proportion at all. `aria-valuenow` is
22
- * omitted with it, which is the platform's own signal for the same thing.
23
- *
24
- * **The fill is a fraction of the track, written as a custom property.**
25
- * `--progress-fraction` is derived from `value` and `max` rather than measured,
26
- * so it renders on a server with no layout and needs nothing after upgrade. That
27
- * is what keeps it out of the class of geometry this library writes only on the
28
- * client. A consumer never sets it: it carries a number this element computed
29
- * and CSS cannot, which is what lets a stylesheet draw the thing without
30
- * measuring anything itself.
31
- *
32
- * **`value` is clamped, and out-of-range input is not an error.** A count that
33
- * runs past its own total is ordinary in real applications, and a bar that
34
- * overflows its track or reports `aria-valuenow` above `aria-valuemax` is worse
35
- * than one that reads as finished.
36
- *
37
- * Semantics are on the host through `ElementInternals`, the same as everywhere
38
- * else: a `role` in the shadow root announces correctly and leaves the host
39
- * anonymous to anything walking the light DOM, and a `role` written onto the
40
- * host as an attribute is one a server's markup never had.
41
- *
42
- * @since 0.1.0
43
- * @status stable
44
- * @category feedback
45
- * @aliases Progress bar, Loading bar
46
- * @documentation https://helix.fusion.dev/components/progress
47
- *
48
- * @csspart groove - The unfilled circle, under the ring's own sweep.
49
- * @csspart indicator - The part of it that is done.
50
- * @csspart track - The full length the work is measured against.
51
- * @csspart value - The reading beside the bar, under `show-value`.
52
- *
53
- * @cssproperty --progress-fraction - The value as a fraction of the range, 0 to 1. Pinned to 1 while indeterminate.
54
- */
55
10
  let HelixHeadlessProgress = class HelixHeadlessProgress extends HelixElement {
56
11
  constructor() {
57
12
  super(...arguments);
58
- /**
59
- * Whether the work has a knowable end.
60
- *
61
- * On the headless tier rather than as a treatment, because it is
62
- * semantics before it is a picture: a progressbar with no `aria-valuenow` is
63
- * how the platform says "running, and nobody knows how far", and a reader on
64
- * a screen reader is told that rather than being read a number that means
65
- * nothing. `value` and `max` are ignored while it is set.
66
- *
67
- * Not `Loader`, which is the other thing this could be. A loader says the
68
- * page is busy; a bar says a named piece of work is running, and keeps its
69
- * place in the layout when the answer arrives and it becomes determinate.
70
- */
71
13
  this.indeterminate = false;
72
- /**
73
- * Accessible name for what is in progress.
74
- */
75
14
  this.label = 'Progress';
76
- /**
77
- * The total the work is measured against.
78
- */
79
15
  this.max = 100;
80
- /**
81
- * Whether the reading is shown beside the bar.
82
- *
83
- * The same name and shape `Slider` uses, because it is the same question:
84
- * a control that holds a number can either show it or leave it to whatever is
85
- * around it. Off by default, since most bars sit under a label that already
86
- * says what is happening and a second number is noise.
87
- *
88
- * Nothing under `indeterminate`, where there is no reading to show: a percent
89
- * that does not move is worse than none, and `valueText` is the property for
90
- * saying "uploading" while the bar runs.
91
- */
92
16
  this.showValue = false;
93
- /**
94
- * What has been done so far, in the same units as `max`.
95
- *
96
- * Clamped between zero and `max` for both the fill and the announcement, so
97
- * the two can never disagree about what is on screen.
98
- */
99
17
  this.value = 0;
100
- /**
101
- * A reading for the value, where the number alone does not say enough.
102
- *
103
- * "3 of 8 files" rather than "37". Announced instead of the percentage, and
104
- * never instead of the value itself: `aria-valuenow` still carries the
105
- * number, because assistive technology that ignores the text still has to
106
- * report progress.
107
- */
108
18
  this.valueText = '';
109
19
  }
110
- /** What is done, as a fraction of the whole, between 0 and 1. */
111
20
  get fraction() {
112
21
  const max = this.max > 0 ? this.max : 100;
113
- /* `|| 0` catches the value that is not a number.
114
- *
115
- * `undefined / 100` is `NaN`, `Math.min(Math.max(NaN, 0), 1)` is `NaN`, and
116
- * `NaN` reached CSS as `scale: NaN`, which is an invalid value the
117
- * declaration is dropped for: the bar fell back to its unscaled width and
118
- * painted COMPLETE for a value nobody had given. `aria-valuenow` announced
119
- * the string "NaN" beside it. Empty means nothing done, which is the only
120
- * reading that is not a claim. */
121
22
  return Math.min(Math.max(this.value / max, 0), 1) || 0;
122
23
  }
123
24
  willUpdate(changed) {
124
25
  super.willUpdate(changed);
125
26
  hostAria(this.internals, { label: this.label, role: 'progressbar' });
126
- /* The clamped value, not the given one, so what is announced is what is
127
- drawn. Written as strings because that is what `ElementInternals` takes;
128
- a number assigned here is stringified anyway and reads as though the
129
- type were checked. */
130
27
  this.internals.ariaValueMin = '0';
131
28
  this.internals.ariaValueMax = String(this.max > 0 ? this.max : 100);
132
- /* Absent while indeterminate, not zero. An omitted `aria-valuenow` is the
133
- * platform's own signal for indeterminate, and it is the only one:
134
- * `aria-busy` says the element is being updated, which is a different claim.
135
- * Reporting 0 would announce "0 percent" for work that is running, which is
136
- * worse than saying nothing because it is wrong rather than unknown.
137
- *
138
- * Otherwise the clamped value itself, never the fraction multiplied back up.
139
- * Dividing by `max` and then multiplying by it is a round trip through a
140
- * float, and for roughly one integer pair in sixteen it does not come back:
141
- * `1/3*3` is `0.9999999999999998`, which is what the accessibility tree
142
- * carried and what a screen reader read out. */
143
29
  const ceiling = this.max > 0 ? this.max : 100;
144
30
  this.internals.ariaValueNow = this.indeterminate
145
31
  ? null
@@ -147,20 +33,7 @@ let HelixHeadlessProgress = class HelixHeadlessProgress extends HelixElement {
147
33
  this.internals.ariaValueText = this.valueText || null;
148
34
  }
149
35
  render() {
150
- /* A custom property rather than a width, so the styled tier decides what
151
- the number means: a bar fills along it, and a ring would sweep. */
152
36
  const fraction = this.indeterminate ? 1 : this.fraction;
153
- /* Two circles and `pathLength`, which is what makes the arithmetic go away.
154
- *
155
- * Declaring the path 100 units long lets `stroke-dasharray` be written as
156
- * the percentage itself, so nothing here or in the stylesheet needs the
157
- * circumference, the radius or pi. The geometry is CSS as well: `r` and the
158
- * stroke both read the ring's own custom properties, so a consumer changing
159
- * the thickness moves the circle that carries it without the component
160
- * recalculating anything.
161
- *
162
- * `aria-hidden`, because the host is the `progressbar` and carries the
163
- * value. The drawing is a picture of what it already says. */
164
37
  const ring = html `<svg part="track" aria-hidden="true">
165
38
  <circle part="groove" />
166
39
  <circle part="indicator" pathLength="100" style="--progress-fraction: ${fraction}" />
@@ -170,12 +43,7 @@ let HelixHeadlessProgress = class HelixHeadlessProgress extends HelixElement {
170
43
  : html `<div part="track">
171
44
  <div part="indicator" style="--progress-fraction: ${fraction}"></div>
172
45
  </div>`}
173
- ${
174
- /* `aria-hidden`, because the bar already reports this. The host carries
175
- `role="progressbar"` with `aria-valuenow` and `aria-valuetext`, so a
176
- readable copy of the same number is a second announcement of one
177
- fact. It is here to be looked at, not to be read out. */
178
- this.showValue && !this.indeterminate
46
+ ${this.showValue && !this.indeterminate
179
47
  ? html `<span part="value" aria-hidden="true"
180
48
  >${this.valueText || `${Math.round(this.fraction * 100)}%`}</span
181
49
  >`
@@ -8,26 +8,10 @@ import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { encode } from 'uqr';
11
- /** Whether a module is one of the three finder squares, or their separators. */
12
11
  const isFinder = (x, y, size) => {
13
12
  const near = (a, b) => a < 8 && b < 8;
14
13
  return near(x, y) || near(size - 1 - x, y) || near(x, size - 1 - y);
15
14
  };
16
- /**
17
- * The dark modules of a matrix, as one path.
18
- *
19
- * One `<path>` rather than a rect per module. A modest code is 33 by 33, so a
20
- * rect apiece is a thousand elements the browser lays out and the accessibility
21
- * tree walks, and every one of them is a one-unit square.
22
- *
23
- * **`square` joins runs and the other styles cannot.** A row of eight square
24
- * modules is one `h8`; a row of eight *rounded* ones is eight shapes, because
25
- * the corners are what is being drawn and a joined run has none in the middle.
26
- *
27
- * `only` splits the drawing in two without walking the matrix twice: the finder
28
- * squares are structural, at three known corners, so a caller can ask for them
29
- * alone and get a second path to paint differently.
30
- */
31
15
  const pathFor = (matrix, style, only = 'all') => {
32
16
  const size = matrix.length;
33
17
  const wanted = (x, y) => {
@@ -56,10 +40,6 @@ const pathFor = (matrix, style, only = 'all') => {
56
40
  }
57
41
  return parts.join('');
58
42
  }
59
- /* A circle for `dots` and a squircle for `rounded`, both drawn as arcs on a
60
- one-unit square. The radius is a little under half so neighboring dots do
61
- not touch, which is what makes a dotted code read as dots rather than as a
62
- square one with soft edges. */
63
43
  const r = style === 'dots' ? 0.45 : 0.28;
64
44
  for (let y = 0; y < size; y += 1) {
65
45
  for (let x = 0; x < size; x += 1) {
@@ -79,77 +59,14 @@ const pathFor = (matrix, style, only = 'all') => {
79
59
  }
80
60
  return parts.join('');
81
61
  };
82
- /**
83
- * @summary Encodes a short piece of text as a scannable square.
84
- *
85
- * @description
86
- * **The picture is one `<path>`, not a grid of squares.** A code of any useful
87
- * length is a thousand modules, and drawing each as its own rect is a thousand
88
- * nodes for one image. Runs of adjacent dark modules are joined, so a row of
89
- * eight is one horizontal move rather than eight squares.
90
- *
91
- * **It is an image and says so.** `role="img"` with a name, because what a
92
- * reader with a screen reader needs is what the code is *for*, and no amount of
93
- * describing the pattern gives them that. Where nothing is passed the value
94
- * itself is the name, which is right far more often than it looks: most codes
95
- * hold a URL, and a URL read aloud is the destination.
96
- *
97
- * **The quiet zone is part of the code.** Four modules of margin are required
98
- * by the specification, not a design choice, and a scanner will refuse a code
99
- * that is cropped to its edge. It is drawn inside the `viewBox` so it survives
100
- * any size the component is given, including one set in CSS.
101
- *
102
- * `correction` is what buys the middle slot. A code at `H` can lose thirty
103
- * percent of itself and still read, which is what makes covering the center
104
- * with a mark safe; at `L` the same mark makes the code unscannable. That is
105
- * why the slot exists on the same element as the property, and why the default
106
- * moves when the slot is filled.
107
- *
108
- * @since 0.1.0
109
- * @status stable
110
- * @category media
111
- * @aliases QR, Barcode, Scan code
112
- * @documentation https://helix.fusion.dev/components/qr-code
113
- *
114
- * @slot logo - A mark for the center, which only reads at a high correction.
115
- *
116
- * @csspart code - The square itself.
117
- * @csspart finders - The three corner squares, which can take their own color.
118
- * @csspart logo - The box holding the center mark.
119
- * @csspart modules - Everything but the three corner squares.
120
- */
121
62
  let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
122
63
  constructor() {
123
64
  super(...arguments);
124
- /**
125
- * What the image is called.
126
- *
127
- * Falling back to the value is deliberate: a code almost always holds a URL,
128
- * and a URL is a better name than "QR code" for a reader who cannot see it.
129
- */
130
65
  this.label = '';
131
- /**
132
- * How the modules are drawn.
133
- *
134
- * `square` is the default and the only one that joins runs of adjacent
135
- * modules into a single subpath, which is what keeps a thousand modules to a
136
- * handful of nodes. `rounded` and `dots` draw each module on its own, because
137
- * the corners are the thing being drawn and a joined run has none in the
138
- * middle. That cost is paid only by a code that asks for them.
139
- *
140
- * A scanner reads contrast between cells, not their shape, so all three
141
- * scan. `dots` sits a little under half a module so neighbors do not touch,
142
- * which is what makes it read as dots rather than as a softened square code.
143
- */
144
66
  this.shape = 'square';
145
- /**
146
- * What the code encodes, which is usually a URL.
147
- */
148
67
  this.value = '';
149
- /** Whether anything is slotted into the middle. */
150
68
  this.logo = false;
151
69
  }
152
- /** The level actually used, which the slot can raise. */
153
70
  get level() {
154
71
  return this.correction ?? (this.logo ? 'H' : 'M');
155
72
  }
@@ -157,23 +74,10 @@ let HelixHeadlessQrCode = class HelixHeadlessQrCode extends HelixElement {
157
74
  if (this.value === '') {
158
75
  return nothing;
159
76
  }
160
- /* Regenerated on every render rather than cached. It is a pure function of
161
- two properties, Lit only renders when one of them moved, and a cache here
162
- would be a second place for them to disagree. */
163
77
  const result = encode(this.value, { border: 0, ecc: this.level });
164
78
  const size = result.size;
165
- /* The quiet zone, in the `viewBox` rather than in a wrapper's padding. Four
166
- modules is what the specification requires and what a scanner looks for,
167
- so it has to scale with the code and not with whatever box the component
168
- was given. */
169
79
  const quiet = 4;
170
80
  const box = size + quiet * 2;
171
- /* The three corner squares as their own path, so they can take their own
172
- color. Their positions are structural rather than guessed: seven by
173
- seven at each of three corners, plus the separator ring, which is what
174
- `isFinder` describes. Drawn square whatever the body's style is, because
175
- they are what a scanner locks onto first and rounding them costs
176
- reliability for decoration. */
177
81
  const finders = pathFor(result.data, 'square', 'finders');
178
82
  return svg `<svg
179
83
  part="code"
@@ -7,34 +7,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary A short quotation, set inline with the text around it.
12
- *
13
- * @description
14
- * Renders a real `<q>`, which is what supplies the quotation marks: the browser
15
- * inserts the right ones for the document's language, and nests them correctly
16
- * when one quotation sits inside another. Typing the marks into the content
17
- * instead gets both of those wrong in every language but one.
18
- *
19
- * A passage set off from the prose rather than run inside it is a blockquote,
20
- * which is a different element for a different job.
21
- *
22
- * @since 0.1.0
23
- * @status stable
24
- * @category typography
25
- * @aliases Q, InlineQuote
26
- * @documentation https://helix.fusion.dev/components/quote
27
- *
28
- * @slot - The quoted text.
29
- *
30
- * @csspart quote - The quotation.
31
- */
32
10
  let HelixHeadlessQuote = class HelixHeadlessQuote extends HelixElement {
33
11
  constructor() {
34
12
  super(...arguments);
35
- /**
36
- * URL identifying where the quotation came from.
37
- */
38
13
  this.cite = '';
39
14
  }
40
15
  render() {
@@ -8,70 +8,13 @@ import { customElement, DisabledCascadeController, HelixFormElement, hostAria, K
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessRadio } from '../root/radio-root.js';
11
- /**
12
- * @summary A set of choices where exactly one is the answer.
13
- *
14
- * @description
15
- * **The group is the control, and the radios are its options.** This is the
16
- * asymmetry with checkboxes and it comes from what a form submits: a set of
17
- * checkboxes is several answers and each box submits its own, while a set of
18
- * radios is one answer, so the group owns the `name`, the `value` and the
19
- * validity, and a lone radio outside a group is not a control at all.
20
- *
21
- * Which is also why `MixinField` lands here and not on `Radio`: the label, the
22
- * hint and the message are about the question, and the question is the set.
23
- *
24
- * Selection travels up and state travels down. An item announces that it was
25
- * chosen, the group decides whether that stands, and the group writes
26
- * `selected` onto every item. Nothing sets itself, so there is one answer to
27
- * "which one is on" rather than one per item.
28
- *
29
- * `value` and `defaultValue` are separate for the reason `checked` and
30
- * `defaultChecked` are: `value` is the answer now, `defaultValue` is what the
31
- * markup asked for, which is where a reset returns and is gone the moment
32
- * anything is chosen.
33
- *
34
- * @since 0.1.0
35
- * @status experimental
36
- * @aliases RadioButtons, Choice group
37
- * @documentation https://helix.fusion.dev/components/radio-group
38
- *
39
- * @slot - The radios.
40
- * @slot hint - Richer than a string.
41
- * @slot label - Richer than a string, where the question is more than one.
42
- *
43
- * @event {Event} change - Announced once the value has moved.
44
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired when the answer changes, and cancelable.
45
- * @event {Event} input - Announced once the value has moved.
46
- *
47
- * @csspart feedback - What is wrong with it.
48
- * @csspart hint - Help under the set.
49
- * @csspart items - The choices, which is what `orientation` lays out.
50
- * @csspart label - The question, over the set.
51
- */
52
11
  let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControllerValue(MixinField(HelixFormElement)) {
53
12
  constructor() {
54
13
  super(...arguments);
55
- /* Moving and choosing are one act, which is the radio group pattern and is
56
- what this controller exists to stop being written per component. */
57
- /* Registered rather than held. A controller adds itself to the host, so there
58
- is no reference to keep, and nothing here talks to it after construction.
59
- `protected` rather than `private` because a private field nothing reads is
60
- an unused one, and the subclass tier is where a consumer would reach for
61
- it. */
62
14
  this.keys = new KeyboardNavigationController(this, { items: () => this.items });
63
- /* Stated once, and the group never thinks about it again. Hand calls from
64
- each method that could disable an item reach different paths per
65
- component; the controller is what stops that being written per group. */
66
15
  this.cascade = new DisabledCascadeController(this, {
67
16
  items: () => this.items,
68
17
  });
69
- /**
70
- * Announces before it applies, so a listener can refuse the change.
71
- *
72
- * The item has not moved by the time this runs, unlike a native input, so
73
- * refusing is simply not writing the value: there is nothing to put back.
74
- */
75
18
  this.handleSelection = (event) => {
76
19
  event.stopPropagation();
77
20
  const next = event.detail.value;
@@ -81,25 +24,14 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
81
24
  this.requestValue(next);
82
25
  };
83
26
  }
84
- /** The radios, in light-DOM order. Client only: the server has no tree. */
85
27
  get items() {
86
28
  return [...this.children].filter((child) => child instanceof HelixHeadlessRadio);
87
29
  }
88
- /** Writes the answer onto the items, and nothing onto the host. */
89
30
  place() {
90
31
  const items = this.items;
91
32
  for (const item of items) {
92
33
  item.selected = item.value === this.value;
93
34
  }
94
- /* Nothing chosen yet, so the first item is the tab stop. A group where
95
- every item is `tabindex="-1"` is a group the keyboard cannot enter, and
96
- an answered group that keeps the entry stop carries two, so tabbing
97
- through a form stops twice on one question and the first stop is on an
98
- item that is not the answer.
99
-
100
- `entered` rather than a bare toggle, because this also runs when the
101
- group is answered and the attribute has to go whether or not this call is
102
- the one that put it there. */
103
35
  const entered = this.value === '' && items.length > 0 ? items[0] : undefined;
104
36
  for (const item of items) {
105
37
  if (item === entered) {
@@ -110,7 +42,6 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
110
42
  }
111
43
  }
112
44
  }
113
- /** Reports the answer and the verdict on it. */
114
45
  sync() {
115
46
  this.internals.setFormValue(this.value === '' ? null : this.value);
116
47
  this.reportRequired(this.value === '', 'Choose one to continue.', 'items');
@@ -125,16 +56,7 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
125
56
  }
126
57
  willUpdate(changed) {
127
58
  super.willUpdate(changed);
128
- // Through internals, so nothing lands on the host as an attribute a React
129
- // consumer's server markup never had.
130
- /* The string half, which is what a server can write. `linkHost` adds the
131
- element references on the client, and those win where a consumer slotted
132
- a label richer than a string. */
133
59
  hostAria(this.internals, { label: this.label, role: 'radiogroup' });
134
- /* `required` reflects, sets validity and paints the asterisk the field
135
- sheet draws, and tells the accessibility tree nothing on its own. Without
136
- this a reader meets a mark beside the label they cannot see, and no
137
- announcement of why the form would refuse the group. */
138
60
  this.internals.ariaRequired = this.required ? 'true' : null;
139
61
  }
140
62
  updated(changed) {
@@ -144,9 +66,6 @@ let HelixHeadlessRadioGroup = class HelixHeadlessRadioGroup extends MixinControl
144
66
  this.linkHost();
145
67
  }
146
68
  render() {
147
- /* The choices sit in an element of their own, because `orientation` lays
148
- out whatever is in the flex container and on the host that would be the
149
- label and the hint as well. */
150
69
  return html `${this.renderGroupLabel()}
151
70
  <div part="items">
152
71
  <slot @slotchange=${() => this.place()}></slot>