@fusion.dev/helix-headless 0.2.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
@@ -10,64 +10,14 @@ import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { HelixHeadlessAccordion } from '../root/accordion-root.js';
13
- /** Shadow-DOM id linking the trigger's aria-controls to the content region. */
14
13
  const CONTENT_ID = 'content';
15
- /**
16
- * Shadow-DOM id of the trigger, used to name the content region.
17
- *
18
- * A `role="region"` with no accessible name is not exposed as a landmark by
19
- * screen readers and is flagged by every automated audit, so the region borrows
20
- * the trigger's text. Both ids live in the same shadow root, so the reference
21
- * resolves; an IDREF pointing at the light DOM never would.
22
- */
23
14
  const TRIGGER_ID = 'trigger';
24
- /**
25
- * @summary One collapsible section of an accordion.
26
- *
27
- * @description
28
- * Carries its own open state, which is why it is an element rather than a slot.
29
- * Used alone, outside an accordion, it is a disclosure.
30
- *
31
- * Content stays mounted when closed, so it can animate and so the text is
32
- * present in server-rendered HTML. Closed content carries both `hidden` and
33
- * `inert`: the first collapses it with no stylesheet in play, the second keeps
34
- * it out of the tab order and the accessibility tree once the styled tier's
35
- * `display` has overridden the first.
36
- *
37
- * @since 0.1.0
38
- * @status experimental
39
- * @aliases Collapsible, Expander, Disclosure
40
- * @documentation https://helix.fusion.dev/components/accordion
41
- *
42
- * @slot - The content revealed when open.
43
- * @slot summary - The always-visible trigger content.
44
- *
45
- * @event {HelixBeforeCloseEvent} helix-before-close - Fired before the section closes, and cancelable.
46
- * @event {HelixBeforeOpenEvent} helix-before-open - Fired before the section opens. Cancelable: refusing it takes the decision and nothing moves.
47
- * @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
48
- *
49
- * @csspart chevron - The open and close indicator.
50
- * @csspart container - The outer wrapper.
51
- * @csspart content - The revealed region.
52
- * @csspart content-inner - The clipping box inside the animated region.
53
- * @csspart trigger - The clickable summary row.
54
- */
55
15
  let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinControllerOpen(HelixElement) {
56
16
  constructor() {
57
17
  super(...arguments);
58
- /**
59
- * Opens or closes this item, and enforces the accordion's mode.
60
- *
61
- * The item that was clicked is the coordinator. It refuses first, then
62
- * announces, then applies, then closes its peers, in that order: closing a
63
- * peer before knowing whether this change was even allowed would leave the
64
- * set collapsed after a listener refused it.
65
- */
66
18
  this.handleToggle = () => {
67
19
  const next = !this.open;
68
20
  const mode = this.accordion?.mode;
69
- // `single` keeps one open at all times, so the last one cannot be closed.
70
- // `single-collapsible` is the same rule without that floor.
71
21
  if (!next && mode === 'single') {
72
22
  return;
73
23
  }
@@ -77,15 +27,12 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
77
27
  if (next && (mode !== undefined || (this.name !== undefined && this.name !== ''))) {
78
28
  for (const peer of this.peers) {
79
29
  if (peer !== this && peer.open) {
80
- // A peer whose consumer refuses the request stays open, which can
81
- // legitimately leave two open.
82
30
  peer.requestOpen(false);
83
31
  }
84
32
  }
85
33
  }
86
34
  };
87
35
  }
88
- /** The chevron, or nothing when suppressed. */
89
36
  renderChevron() {
90
37
  if (this.chevronPlacement === 'none') {
91
38
  return nothing;
@@ -94,30 +41,11 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
94
41
  ${unsafeSVG(caretDown.content)}
95
42
  </svg>`;
96
43
  }
97
- /**
98
- * The nearest accordion above this item, if there is one.
99
- *
100
- * Resolved on demand rather than held, so an item moved between accordions
101
- * coordinates with the one it is in now. `closest` crosses no shadow
102
- * boundary, which is correct: an item inside another item's content is in
103
- * that item's light DOM and finds the same accordion, so the check below is
104
- * what keeps it independent.
105
- */
106
44
  get accordion() {
107
45
  return closestInstance(this, HelixHeadlessAccordion);
108
46
  }
109
- /**
110
- * The items this one coordinates with: its own siblings, and only those.
111
- *
112
- * Direct children only. An accordion nested inside an item's content is a
113
- * separate set, and opening a section in it must not close anything outside.
114
- */
115
47
  get peers() {
116
48
  if (this.name !== undefined && this.name !== '') {
117
- /* By name, across the whole root rather than one accordion, which is what
118
- `<details name>` means. `getRootNode()` so an item inside a shadow tree
119
- finds its group there rather than reaching into the document, which is
120
- the same boundary every IDREF in this library respects. */
121
49
  const root = this.getRootNode();
122
50
  return [
123
51
  ...root.querySelectorAll(`${this.localName}[name="${CSS.escape(this.name)}"]`),
@@ -129,45 +57,9 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
129
57
  }
130
58
  return accordion.items;
131
59
  }
132
- /**
133
- * Moves focus to this item's trigger.
134
- *
135
- * The parent accordion drives arrow-key navigation and cannot reach the
136
- * trigger itself, because it is inside this item's shadow root. Exposing the
137
- * intent as a method keeps the parent out of the child's internals.
138
- */
139
60
  focusTrigger() {
140
61
  this.renderRoot.querySelector(`#${TRIGGER_ID}`)?.focus();
141
62
  }
142
- /**
143
- * The item, with `hidden` doing the collapsing.
144
- *
145
- * **A closed section has to be closed with no stylesheet at all**, which is
146
- * the whole claim `headless` makes. `inert` alone is not that: it takes the
147
- * region out of the accessibility tree and off the tab order and leaves every
148
- * pixel of it on the screen, so a consumer taking this tier gets sections of
149
- * text permanently open under a trigger saying `aria-expanded="false"`, and a
150
- * sighted reader sees content a screen reader is told is not there. Leaving
151
- * the collapse to the styled tier's `grid-template-rows: 0fr` puts the state
152
- * in a stylesheet, where only one of the two tiers has it.
153
- *
154
- * **`hidden` costs the styled tier nothing**, which is what makes this the
155
- * cheap fix rather than a trade. The UA sheet hides `[hidden]` from the UA
156
- * origin, and `accordion-item.css` already sets `display: grid` on this part
157
- * unconditionally, so the author rule wins and the 0fr transition runs
158
- * exactly as before. Measured on both tiers.
159
- *
160
- * **Not `hidden="until-found"`**, which would be the better accordion and is
161
- * a different piece of work. It hides through `content-visibility` rather
162
- * than `display`, so the styled tier's `display: grid` would no longer
163
- * override it and a collapsed section would animate open to nothing. It also
164
- * needs a `beforematch` handler to set `open`, or find-in-page reveals text
165
- * while the trigger still reports collapsed.
166
- *
167
- * `inert` stays. `hidden` answers the reader who can see and `inert` answers
168
- * the one tabbing through, and in the styled tier, where `display` is
169
- * overridden, `inert` is the only one of the two still in force.
170
- */
171
63
  render() {
172
64
  const open = this.open;
173
65
  return html `
@@ -8,38 +8,7 @@ import { customElement, HelixElement } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessAccordionItem } from '../item/accordion-item.js';
11
- /** Keys that move focus between triggers, per the APG accordion pattern. */
12
11
  const NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'Home', 'End']);
13
- /**
14
- * @summary Groups collapsible sections, one open at a time by default.
15
- *
16
- * @description
17
- * Holds the mode and nothing else. The item that was clicked is what
18
- * coordinates: it walks up to the nearest accordion, reads the mode off it, and
19
- * closes its peers itself.
20
- *
21
- * Coordination is structural, not a subscription. `helix-before-open` does not bubble
22
- * and is not composed, because overlays nest and an inner one's lifecycle must
23
- * never read as the outer one's, so an accordion cannot learn that a child
24
- * opened by listening for it.
25
- *
26
- * Membership resolves on demand, so there is no registry to keep in step: an
27
- * item added a second after render coordinates on its first click.
28
- *
29
- * Items are matched with `instanceof HelixHeadlessAccordionItem`, never by tag
30
- * name. Matching `helix-accordion-item` would tie this tier to the styled one.
31
- *
32
- * Every listener is attached on connect, so none of it runs on the server,
33
- * where there is no tree to walk and no events to hear.
34
- *
35
- * @since 0.1.0
36
- * @status experimental
37
- * @category layout
38
- * @aliases Collapse, Expander group
39
- * @documentation https://helix.fusion.dev/components/accordion
40
- *
41
- * @slot - The accordion's items.
42
- */
43
12
  let HelixHeadlessAccordion = class HelixHeadlessAccordion extends HelixElement {
44
13
  constructor() {
45
14
  super(...arguments);
@@ -66,12 +35,10 @@ let HelixHeadlessAccordion = class HelixHeadlessAccordion extends HelixElement {
66
35
  else {
67
36
  next = last;
68
37
  }
69
- // Only once a move is certain, so an unhandled key still scrolls the page.
70
38
  event.preventDefault();
71
39
  items[next]?.focusTrigger();
72
40
  };
73
41
  }
74
- /** Direct items, in light-DOM order. Client only: the server has no tree. */
75
42
  get items() {
76
43
  return [...this.children].filter((child) => child instanceof HelixHeadlessAccordionItem);
77
44
  }
@@ -9,50 +9,9 @@ import { x } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- /**
13
- * @summary A page-level message above the masthead, which can be dismissed.
14
- *
15
- * @description
16
- * Chrome rather than content. An announcement sits above the header and speaks
17
- * for the whole page: a trial ending, a service degraded, a change of terms. A
18
- * passage that interrupts a document to set itself apart is a `Callout`, which
19
- * is read where it sits and cannot be dismissed. Neither is a variant of the
20
- * other.
21
- *
22
- * **Always a polite live region.** `role="alert"` is assertive and interrupts
23
- * whatever is being read, which is right for something that appears mid-session
24
- * and wrong for something that was in the served HTML: an assertive
25
- * announcement would cut across a screen-reader user on every page load, before
26
- * they had read anything. Politeness costs a moment and is never wrong; the tone
27
- * does not change that, because an error that was always there is still not an
28
- * interruption.
29
- *
30
- * Dismissing is a request. The announcement does not remove itself, because the
31
- * consumer owns the markup and taking the node out from under them would put
32
- * their state and the document out of step. `preventDefault()` on the
33
- * `helix-dismiss` is a refusal.
34
- *
35
- * @since 0.1.0
36
- * @status stable
37
- * @category feedback
38
- * @aliases Banner, Notification bar, Alert bar
39
- * @documentation https://helix.fusion.dev/components/announcement
40
- *
41
- * @slot - The message.
42
- * @slot actions - What to do about it, usually a link or a button.
43
- * @slot icon - Replaces the glyph inside the dismiss control.
44
- *
45
- * @event {HelixDismissEvent} helix-dismiss - Fired when the reader asks to close it. Cancelable: refusing it takes the decision and nothing moves.
46
- *
47
- * @csspart actions - The trailing action region.
48
- * @csspart content - The message region.
49
- * @csspart dismiss - The control that asks to close.
50
- * @csspart dismiss-mark - The glyph on the dismiss control, where none is slotted.
51
- */
52
12
  let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDismissible(HelixElement) {
53
13
  willUpdate(changed) {
54
14
  super.willUpdate(changed);
55
- // On the host, and always polite. See the note above on `role="alert"`.
56
15
  hostAria(this.internals, { role: 'status' });
57
16
  this.internals.ariaLive = 'polite';
58
17
  }
@@ -60,39 +19,12 @@ let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDis
60
19
  super.updated(changed);
61
20
  this.markEmpty();
62
21
  }
63
- /**
64
- * Says on the `actions` region whether anything is in it.
65
- *
66
- * An empty region is not free: it is still a flex item, so it still takes the
67
- * container's gap. There is no selector that can ask, `slot:empty` being
68
- * about a slot's fallback rather than its assigned nodes, so the component
69
- * asks and says so, on the region inside this shadow root rather than on the
70
- * host: an attribute written onto the host is a change to markup the consumer
71
- * rendered, which React reports on hydration and does not patch up.
72
- */
73
22
  markEmpty() {
74
23
  const region = this.renderRoot?.querySelector('[part="actions"]');
75
24
  const empty = !slotFilled(this.renderRoot?.querySelector('slot[name="actions"]'));
76
25
  region?.toggleAttribute('data-empty', empty);
77
- /* `hidden` beside the mirror, because `data-empty` is a hook the styled
78
- sheet reads and this tier has no sheet: an empty region left in the box
79
- tree is a gap a consumer cannot close without knowing to look for it. The
80
- styled rule stays, because that sheet sets `display` on this part and
81
- therefore outranks the UA one. */
82
26
  region?.toggleAttribute('hidden', empty);
83
27
  }
84
- /**
85
- * The cross inside the dismiss button, behind a seam.
86
- *
87
- * Slot fallback, so a consumer replaces it by slotting; and a method, so the
88
- * tier above replaces it by overriding. Those answer different people: the
89
- * slot is for the page and the method is for whoever is building the styled
90
- * layer, and a fallback with only the first is one a tier can restyle and
91
- * cannot change.
92
- *
93
- * Not `renderIcon`, which is what `Toast` and `Callout` call the method that
94
- * draws a tone mark. An announcement has no tone mark at all.
95
- */
96
28
  renderDismissIcon() {
97
29
  return html `<slot name="icon">
98
30
  <svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
@@ -103,14 +35,7 @@ let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDis
103
35
  render() {
104
36
  return html `<div part="content"><slot></slot></div>
105
37
  <div part="actions"><slot name="actions"></slot></div>
106
- ${
107
- /* A plain `<button>`, the way `Menu` renders its way back and every
108
- other in-shadow control in the library. Never a nested component
109
- here: `[part="dismiss"]` would select the host and stop at its shadow
110
- boundary, so the styled tier could not reach the inner control at
111
- all, and the parent would depend invisibly on the child being
112
- registered. */
113
- this.dismissible
38
+ ${this.dismissible
114
39
  ? html `<button
115
40
  part="dismiss"
116
41
  type="button"
@@ -7,87 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  import { customElement, HelixElement, HelixScrolledEvent, hostAria } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
- /**
11
- * @summary Page-level banner holding brand, navigation, and actions.
12
- *
13
- * @description
14
- * Tracks whether the page has scrolled and carries it as the custom state
15
- * `scrolled`, so the styled tier can lift the bar off the page without a scroll
16
- * handler of its own. Scroll position is the one piece of state the server
17
- * genuinely cannot know, so it is read on connect rather than derived: a fresh
18
- * load starts at the top, which is what the server rendered, and a reload
19
- * part-way down corrects on hydration.
20
- *
21
- * **A custom state and never `data-scrolled`.** An attribute the element writes
22
- * onto its own host is one the server did not send, so a hydrating framework
23
- * reports the tree as mismatched, intermittently: only when the reader reloads
24
- * far enough down the page to pass the threshold. A consumer can answer that
25
- * only by suppressing hydration warnings on an element they did not write.
26
- * `ElementInternals.states` is deliberately not in the DOM, so there is nothing
27
- * for a hydration pass to disagree with, and `:state(scrolled)` selects it. The
28
- * base class made the same move for `data-settled` and its note explains the
29
- * rest.
30
- *
31
- * The listener is passive and reads `scrollY` only, so it never blocks
32
- * scrolling.
33
- *
34
- * Three regions, each in its own wrapper, because the row is a grid and a grid
35
- * needs three items to put the middle one in the middle. A bare slot is
36
- * `display: contents`, so its assigned elements become grid items themselves:
37
- * two actions would take two tracks and push the navigation off center. These
38
- * wrappers earn their boxes, which is the test; they carry no inset or fill of
39
- * their own, so an empty one still paints nothing.
40
- *
41
- * The host is the banner and spans the viewport; the row inside it is what gets
42
- * centered. A banner that is itself narrowed leaves its background stopping
43
- * short of the window edges, which reads as a floating bar rather than as the
44
- * top of the page. That needs one element and one row, not a wrapper as well:
45
- * the host carries the material and `content` carries the measure.
46
- *
47
- * @since 0.1.0
48
- * @status stable
49
- * @category navigation
50
- * @aliases Masthead, Navbar, TopBar
51
- * @documentation https://helix.fusion.dev/components/app-header
52
- *
53
- * @slot - The middle region, usually a `Navigation`.
54
- * @slot end - Trailing content.
55
- * @slot start - Leading content, usually a wordmark.
56
- *
57
- * @event {HelixScrolledEvent} helix-scrolled - Fired when the page crosses the
58
- * threshold, with the new state. A custom state cannot be observed the way an
59
- * attribute could, so the header says when it changes rather than leaving
60
- * consumers to run a second scroll listener and disagree about the hysteresis.
61
- *
62
- * @csspart content - The middle region.
63
- * @csspart end - The trailing region.
64
- * @csspart row - The centered row inside the banner.
65
- * @csspart start - The leading region.
66
- */
67
10
  let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
68
11
  constructor() {
69
12
  super(...arguments);
70
- /* Whether the bar currently believes the page has moved, kept alongside the
71
- custom state so a scroll that changes nothing writes nothing. */
72
13
  this.scrolled = false;
73
- /**
74
- * A band rather than a line, so the fill cannot flicker at the boundary.
75
- *
76
- * **`scrollY > 0` is a threshold with no width, and a threshold with no width
77
- * is a switch sitting exactly where scroll positions cluster.** Zero is not a
78
- * quiet place: rubber-banding overshoots past it and comes back, scroll
79
- * anchoring nudges by a pixel when an image or a font lands, and a
80
- * `scroll-behavior: smooth` jump settles by oscillating around its target.
81
- * Every one of those crossings toggles the state, and the fill and the blur
82
- * are both transitioned, so each toggle starts a fresh animation in the
83
- * opposite direction. What a reader sees is a bar that shimmers, and text
84
- * legible through it for as long as the fill is part way in.
85
- *
86
- * Eight pixels to take the fill, two to give it back. Asymmetric on purpose:
87
- * the bar should look attached to the top of the page for as long as it
88
- * honestly is, so leaving is nearly the whole way back, and arriving needs
89
- * enough travel to mean the reader moved rather than the layout did.
90
- */
91
14
  this.handleScroll = () => {
92
15
  const y = window.scrollY;
93
16
  const next = this.scrolled ? y > 2 : y > 8;
@@ -95,23 +18,16 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
95
18
  return;
96
19
  }
97
20
  this.scrolled = next;
98
- /* The state is what CSS selects; the event is what anything outside this
99
- element can hear. Both, because neither covers the other: a state cannot
100
- be observed and an event cannot be styled. */
101
21
  this.toggleState('scrolled', next);
102
22
  this.dispatchEvent(new HelixScrolledEvent({ scrolled: next }));
103
23
  };
104
24
  }
105
25
  willUpdate(changed) {
106
26
  super.willUpdate(changed);
107
- // On the host, since the host is the banner.
108
27
  hostAria(this.internals, { role: 'banner' });
109
28
  }
110
29
  connectedCallback() {
111
30
  super.connectedCallback();
112
- // Never reached on the server, which is correct: there is no scroll
113
- // position to read there, and the un-scrolled markup it emits is what a
114
- // fresh load actually shows.
115
31
  window.addEventListener('scroll', this.handleScroll, { passive: true });
116
32
  this.handleScroll();
117
33
  }
@@ -7,89 +7,16 @@ 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
- * The largest `max` the styled tier can hide past.
12
- *
13
- * The fold is `:host([max="N"]) ::slotted(:nth-child(n + N+1))`, one selector
14
- * per value, because `An+B` takes a literal and no custom property can be read
15
- * into it. So the range is finite and this is where it is written down, in the
16
- * tier that has to agree with it: a count that folds and a stylesheet that does
17
- * not would put a "+5" beside five avatars that are still on screen.
18
- */
19
10
  export const FOLDS_UP_TO = 12;
20
- /**
21
- * @summary Overlapping avatars, with a count for the ones not shown.
22
- *
23
- * @description
24
- * A part of `Avatar` rather than a family of its own: it arranges avatars and
25
- * is about nothing else, and its member is a complete `Avatar` the group did
26
- * not have to explain.
27
- *
28
- * **The overflow count is the group's, not a slot.** Which avatars are hidden
29
- * is a fact the group works out from `max` and how many it was given, so a
30
- * consumer writing the "+3" themselves would be writing a number that has to
31
- * agree with a list they do not control.
32
- *
33
- * **Hidden, not removed.** Past `max`, the extra avatars stay in the DOM and
34
- * stop being shown, so nothing re-renders when `max` changes and the accessible
35
- * name can still be built from all of them. The count is what a sighted reader
36
- * gets; the group's own name is what everyone else gets, and "Seven people" is
37
- * more useful than seven names read in a row.
38
- *
39
- * **And hidden by the stylesheet, not by this class.** Hiding from `updated` is
40
- * one update too late and one element too far out: Lit renders on a microtask,
41
- * so an avatar appended past the limit is laid out and painted before anything
42
- * runs to hide it, and marking each one means writing attributes onto markup a
43
- * consumer owns. The styled tier's
44
- * `:host([max="N"]) ::slotted(:nth-child(n + N+1))` is right at the first style
45
- * resolution and writes nothing.
46
- *
47
- * **The fold is finite, and the count agrees with it.** `An+B` takes a literal,
48
- * so there is one selector per value of `max` and the range stops at twelve.
49
- * Past that the group shows everyone, counts nothing and says so, because
50
- * showing everyone is a defensible answer and showing everyone beside a "+5" is
51
- * not. `max` above twelve is a list the consumer should be slicing themselves.
52
- *
53
- * **It is a list.** A row of faces is a set of separate people, and a group
54
- * with no role announces as one run of text. `role="list"` needs its children
55
- * to be `listitem`, which they cannot be told to become without writing an
56
- * attribute onto a consumer's element, so the group carries `role="group"` and
57
- * its name instead: one announcement, correct, and nothing written to anyone
58
- * else's markup.
59
- *
60
- * @since 0.1.0
61
- * @status stable
62
- * @aliases Avatar stack, Facepile
63
- * @documentation https://helix.fusion.dev/components/avatar
64
- *
65
- * @slot - The avatars, in the order they should be read.
66
- *
67
- * @csspart overflow - The count of avatars past `max`.
68
- */
69
11
  let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixElement {
70
12
  constructor() {
71
13
  super(...arguments);
72
- /**
73
- * Accessible name for the set, such as "Reviewers".
74
- */
75
14
  this.label = '';
76
- /** Said once per element, because a re-render is not a second mistake. */
77
15
  this.warned = false;
78
16
  }
79
- /** The avatars, in light-DOM order. Empty on the server, which has no tree. */
80
17
  get avatars() {
81
- /* `?? []` is the server: Lit's element shim carries attributes and not a
82
- tree, so `children` is undefined there and spreading it throws. */
83
18
  return [...(this.children ?? [])].filter((child) => child instanceof HTMLElement);
84
19
  }
85
- /**
86
- * How many avatars are past the limit, and so not shown.
87
- *
88
- * Not `hidden`, which is `HTMLElement`'s own boolean: a getter of that name
89
- * returning a number is a type error, and had it been assignable it would
90
- * have been worse, because a group with three extra members would have set
91
- * the attribute that hides the whole group.
92
- */
93
20
  get overflow() {
94
21
  const limit = this.max ?? 0;
95
22
  const limited = limit > 0 && limit <= FOLDS_UP_TO;
@@ -101,10 +28,6 @@ let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixEleme
101
28
  }
102
29
  updated(changed) {
103
30
  super.updated(changed);
104
- /* Nothing hides anything here: the stylesheet does it, from `max` on the
105
- host. What is left is saying so when a `max` is set that no rule can
106
- match, because showing everyone is a defensible answer and showing
107
- everyone beside a "+5" is not. */
108
31
  const limit = this.max ?? 0;
109
32
  if (limit > FOLDS_UP_TO && this.avatars.length > limit && !this.warned) {
110
33
  this.warned = true;
@@ -7,72 +7,21 @@ 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, state } from 'lit/decorators.js';
10
- /** Initials for a name: first letter of the first and last words. */
11
10
  export const initialsOf = (name) => {
12
11
  const words = name.trim().split(/\s+/).filter(Boolean);
13
12
  if (words.length === 0) {
14
13
  return '';
15
14
  }
16
- /* By code point, not by code unit.
17
- *
18
- * `word[0]` indexes UTF-16, so a name beginning with anything outside the
19
- * basic plane, an emoji or most of the world's less common scripts, yields a
20
- * lone surrogate and the avatar paints U+FFFD. Spreading a string iterates
21
- * code points, which is what a first letter means. */
22
15
  const initial = (word) => [...(word ?? '')][0] ?? '';
23
16
  const first = initial(words[0]);
24
17
  const last = words.length > 1 ? initial(words[words.length - 1]) : '';
25
18
  return (first + last).toUpperCase();
26
19
  };
27
- /**
28
- * @summary Represents a person or entity with an image or their initials.
29
- *
30
- * @description
31
- * Shows `src` until it fails to load. Without an image, or after a failure,
32
- * the default slot is shown; its own fallback is the initials.
33
- *
34
- * The host carries the accessible name, taken from `name`, so the `<img>` has an
35
- * empty `alt` and the initials are hidden from assistive technology. Announcing
36
- * either would repeat the name. An `aria-label` on the host overrides `name`,
37
- * for an avatar standing for something a person's name does not describe.
38
- *
39
- * `load` and `error` are relayed from the host. They are not composed, so they
40
- * stop at the shadow boundary otherwise.
41
- *
42
- * @since 0.1.0
43
- * @status stable
44
- * @category media
45
- * @aliases Profile picture, Gravatar
46
- * @documentation https://helix.fusion.dev/components/avatar
47
- *
48
- * @slot - Shown when there is no usable image, in place of the initials.
49
- *
50
- * @event {Event} error - Relayed from the image when it fails.
51
- * @event {Event} load - Relayed from the image once it has loaded.
52
- *
53
- * @csspart image - The image, when one is loading or loaded.
54
- * @csspart initials - The initials shown when there is no usable image.
55
- */
56
20
  let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
57
21
  constructor() {
58
22
  super(...arguments);
59
- /**
60
- * Name of the person or entity.
61
- *
62
- * Used for the label and the initials.
63
- */
64
23
  this.name = '';
65
- /**
66
- * Image to show.
67
- *
68
- * Falls back to the slot, or to initials, if it fails.
69
- */
70
24
  this.src = '';
71
- /**
72
- * Whether the image failed.
73
- *
74
- * The element's own observation, never set.
75
- */
76
25
  this.failed = false;
77
26
  this.handleError = (event) => {
78
27
  this.failed = true;
@@ -84,22 +33,9 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
84
33
  }
85
34
  willUpdate(changed) {
86
35
  super.willUpdate(changed);
87
- /* A new source is a new attempt.
88
- *
89
- * `failed` is an observation about the image that was loading, so it has to
90
- * be cleared with the source. Latched, an avatar that failed once falls back
91
- * to initials for the life of the element, and a consumer swapping a broken
92
- * URL for a good one, or a list reusing an avatar for the next person, gets
93
- * the initials forever with no error to say why. */
94
36
  if (changed.has('src')) {
95
37
  this.failed = false;
96
38
  }
97
- /* A role only where there is a name to carry.
98
- *
99
- * An unconditional `role="img"` makes an avatar with no `name` an unnamed
100
- * image: a screen reader announces "image" and stops, which is worse than
101
- * announcing nothing, because nothing is what a decorative mark should be.
102
- * With a name it is content and needs the role to hold it. */
103
39
  hostAria(this.internals, {
104
40
  label: this.name,
105
41
  role: this.name === '' ? '' : 'img',
@@ -117,14 +53,6 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
117
53
  }
118
54
  return this.renderInitials();
119
55
  }
120
- /**
121
- * The mark shown where no image loaded, behind a seam a tier can move.
122
- *
123
- * A consumer replaces it by slotting something; a tier above replaces it by
124
- * overriding this, and those answer different people. Written inline it
125
- * answers only the first, and a styled tier wanting initials drawn its own way
126
- * has to reimplement the whole template to reach three elements.
127
- */
128
56
  renderInitials() {
129
57
  return html `<slot>
130
58
  <span part="initials" aria-hidden="true">${initialsOf(this.name)}</span>