@fusion.dev/helix-headless 0.104.1 → 0.105.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.
@@ -4,13 +4,114 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { customElement, HelixElement, HelixMapPanEvent, HelixMapZoomEvent, hostAria, MixinViewport, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
9
- import { property } from 'lit/decorators.js';
7
+ import { customElement, HelixActivateEvent, HelixElement, HelixMapClusterEvent, HelixMapPanEvent, HelixMapPartChangeEvent, HelixMapZoomEvent, hostAria, KeyboardNavigationController, MixinViewport, } from '@fusion.dev/helix-core';
8
+ import { html, nothing } from 'lit';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement) {
11
11
  constructor() {
12
12
  super(...arguments);
13
+ this.cluster = false;
13
14
  this.label = '';
15
+ this.layersLabel = 'Routes, areas, and points';
16
+ this.handlePartChange = (event) => {
17
+ if (event.detail.kind !== 'place') {
18
+ this.requestUpdate();
19
+ }
20
+ };
21
+ this.stop = 0;
22
+ this.keys = new KeyboardNavigationController(this, {
23
+ items: () => this.markers.map((marker, at) => ({
24
+ contains: (node) => node !== null && (marker.contains(node) || marker.shadowRoot?.contains(node) === true),
25
+ disabled: marker.gatheredBy !== undefined,
26
+ focusItem: () => {
27
+ this.stop = at;
28
+ marker.focus();
29
+ },
30
+ select: () => { },
31
+ selected: false,
32
+ })),
33
+ origin: 'focus',
34
+ selects: () => false,
35
+ });
36
+ this.handleFocusIn = (event) => {
37
+ const marker = this.markerOf(event);
38
+ const at = marker ? this.markers.indexOf(marker) : -1;
39
+ if (at !== -1) {
40
+ this.stop = at;
41
+ }
42
+ };
43
+ this.handleClick = (event) => {
44
+ const marker = this.markerOf(event);
45
+ if (marker) {
46
+ this.activate(marker);
47
+ }
48
+ };
49
+ this.handleMarkerKeydown = (event) => {
50
+ const marker = this.markerOf(event);
51
+ if (!marker || (event.key !== 'Enter' && event.key !== ' ')) {
52
+ return;
53
+ }
54
+ event.preventDefault();
55
+ if (event.key === 'Enter' && !event.repeat) {
56
+ this.activate(marker);
57
+ }
58
+ };
59
+ this.handleMarkerKeyup = (event) => {
60
+ const marker = this.markerOf(event);
61
+ if (marker && event.key === ' ') {
62
+ event.preventDefault();
63
+ this.activate(marker);
64
+ }
65
+ };
66
+ this.handleSlotChange = () => {
67
+ this.requestUpdate();
68
+ };
69
+ }
70
+ get layers() {
71
+ return this.knownChildren.filter((child) => child.isMapLayer === true);
72
+ }
73
+ get markers() {
74
+ return this.knownChildren.filter((child) => child.isMapMarker === true);
75
+ }
76
+ markerOf(event) {
77
+ return event
78
+ .composedPath()
79
+ .find((one) => one.isMapMarker === true);
80
+ }
81
+ activate(marker) {
82
+ if (marker.gathered.length > 1) {
83
+ marker.dispatchEvent(new HelixMapClusterEvent({ labels: [...marker.gathered] }));
84
+ return;
85
+ }
86
+ this.dispatchEvent(new HelixActivateEvent({ value: marker.name || marker.label || '' }));
87
+ }
88
+ connectedCallback() {
89
+ super.connectedCallback();
90
+ this.addEventListener(HelixMapPartChangeEvent.eventName, this.handlePartChange);
91
+ this.addEventListener('click', this.handleClick);
92
+ this.addEventListener('focusin', this.handleFocusIn);
93
+ this.addEventListener('keydown', this.handleMarkerKeydown);
94
+ this.addEventListener('keyup', this.handleMarkerKeyup);
95
+ }
96
+ disconnectedCallback() {
97
+ super.disconnectedCallback();
98
+ this.removeEventListener(HelixMapPartChangeEvent.eventName, this.handlePartChange);
99
+ this.removeEventListener('click', this.handleClick);
100
+ this.removeEventListener('focusin', this.handleFocusIn);
101
+ this.removeEventListener('keydown', this.handleMarkerKeydown);
102
+ this.removeEventListener('keyup', this.handleMarkerKeyup);
103
+ }
104
+ updated(changed) {
105
+ super.updated(changed);
106
+ const markers = this.markers;
107
+ const on = markers[Math.min(this.stop, markers.length - 1)];
108
+ const stop = on?.gatheredBy ?? on;
109
+ for (const marker of markers) {
110
+ marker.current = marker === stop;
111
+ }
112
+ if (changed.has('cluster')) {
113
+ this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'place' }));
114
+ }
14
115
  }
15
116
  panEvent(detail) {
16
117
  return new HelixMapPanEvent(detail);
@@ -27,6 +128,15 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
27
128
  this.toggleState('fullscreen', this.filled);
28
129
  this.toggleState('idle', this.resting);
29
130
  }
131
+ renderLayers() {
132
+ const layers = this.layers;
133
+ if (layers.length === 0) {
134
+ return nothing;
135
+ }
136
+ return html `<ul part="layers" aria-label=${this.layersLabel}>
137
+ ${layers.map((layer) => html `<li part="layer">${layer.label ?? ''}${layer.description ? `, ${layer.description}` : ''}</li>`)}
138
+ </ul>`;
139
+ }
30
140
  render() {
31
141
  return html `<div
32
142
  part="region"
@@ -43,8 +153,9 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
43
153
  <slot name="header"></slot>
44
154
  </div>
45
155
  <div part="viewport">
46
- <slot></slot>
156
+ <slot @slotchange=${this.handleSlotChange}></slot>
47
157
  </div>
158
+ ${this.renderLayers()}
48
159
  ${this.renderControls()}
49
160
  <div part="footer">
50
161
  <slot name="footer"></slot>
@@ -55,9 +166,18 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
55
166
  </div>`;
56
167
  }
57
168
  };
169
+ __decorate([
170
+ property({ type: Boolean })
171
+ ], HelixHeadlessMap.prototype, "cluster", void 0);
58
172
  __decorate([
59
173
  property()
60
174
  ], HelixHeadlessMap.prototype, "label", void 0);
175
+ __decorate([
176
+ property({ attribute: 'layers-label' })
177
+ ], HelixHeadlessMap.prototype, "layersLabel", void 0);
178
+ __decorate([
179
+ state()
180
+ ], HelixHeadlessMap.prototype, "stop", void 0);
61
181
  HelixHeadlessMap = __decorate([
62
182
  customElement('helix-headless-map')
63
183
  ], HelixHeadlessMap);
@@ -0,0 +1,67 @@
1
+ import { HelixElement } from '@fusion.dev/helix-core';
2
+ import { nothing, type PropertyValues } from 'lit';
3
+ /** A place a line passes through or a shape is drawn around, in degrees. */
4
+ export interface MapPoint {
5
+ lat: number;
6
+ lng: number;
7
+ }
8
+ /**
9
+ * `points` as it is written: `lat,lng` pairs separated by spaces, the order a
10
+ * marker's own `lat` and `lng` read in. A pair that is not two numbers is
11
+ * dropped rather than drawn at the origin, **an empty side included**:
12
+ * `Number('')` is 0, so `30.1,` from a record with no longitude was a vertex
13
+ * off West Africa.
14
+ */
15
+ export declare const pointsOf: (value: string | null) => MapPoint[];
16
+ /** `points` read from its attribute, which is written and never reflected. */
17
+ export declare const pointsConverter: {
18
+ fromAttribute: typeof pointsOf;
19
+ };
20
+ /**
21
+ * @summary A line on a map, such as a route between stops.
22
+ *
23
+ * @description
24
+ * `connectMap` draws the route as a layer of the map, under its place names,
25
+ * and redraws it when its points, progress, or color change. The frame lists
26
+ * each route by its `label` and `description` for screen readers.
27
+ *
28
+ * @since 0.105.0
29
+ * @status experimental
30
+ * @category media
31
+ * @aliases Map line, Path, Polyline
32
+ * @documentation https://fusion.dev/helix/components/map
33
+ *
34
+ * @event {HelixMapPartChangeEvent} helix-map-part-change - The route's points, progress, name, or color changed. Used by `connectMap`; pages do not need to listen for it.
35
+ */
36
+ export declare class HelixHeadlessMapRoute extends HelixElement {
37
+ /**
38
+ * More about the route, such as its distance or time. Read after its label
39
+ * in the frame's list.
40
+ */
41
+ description?: string;
42
+ /** Marks this as a layer for the frame and the connector, whatever its tag. */
43
+ readonly isMapLayer = true;
44
+ /** @internal Which kind of layer, which is how the connector draws it. */
45
+ readonly layerKind: 'area' | 'route';
46
+ /** The route's name, used in the frame's list. */
47
+ label?: string;
48
+ /**
49
+ * The points the line passes through, in order. Write the attribute as
50
+ * `lat,lng` pairs separated by spaces, or set the property to an array of `{
51
+ * lat, lng }`.
52
+ */
53
+ points?: MapPoint[];
54
+ /**
55
+ * How much of the route has been traveled, from 0 to 1. The traveled part is
56
+ * drawn in a lighter color.
57
+ */
58
+ progress?: number;
59
+ /** Tells the frame and the connector the line moved, was renamed or was retoned. */
60
+ protected updated(changed: PropertyValues): void;
61
+ protected render(): typeof nothing;
62
+ }
63
+ declare global {
64
+ interface HTMLElementTagNameMap {
65
+ 'helix-headless-map-route': HelixHeadlessMapRoute;
66
+ }
67
+ }
@@ -0,0 +1,49 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { customElement, HelixElement, HelixMapPartChangeEvent } from '@fusion.dev/helix-core';
8
+ import { nothing } from 'lit';
9
+ import { property } from 'lit/decorators.js';
10
+ export const pointsOf = (value) => (value ?? '')
11
+ .trim()
12
+ .split(/\s+/)
13
+ .filter(Boolean)
14
+ .map((pair) => pair.split(','))
15
+ .filter((pair) => pair.length === 2 && pair.every((one) => one.trim() !== ''))
16
+ .map((pair) => pair.map(Number))
17
+ .filter((pair) => pair.every((one) => Number.isFinite(one)))
18
+ .map(([lat, lng]) => ({ lat: lat, lng: lng }));
19
+ export const pointsConverter = { fromAttribute: pointsOf };
20
+ let HelixHeadlessMapRoute = class HelixHeadlessMapRoute extends HelixElement {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.isMapLayer = true;
24
+ this.layerKind = 'route';
25
+ }
26
+ updated(changed) {
27
+ super.updated(changed);
28
+ this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'layer' }));
29
+ }
30
+ render() {
31
+ return nothing;
32
+ }
33
+ };
34
+ __decorate([
35
+ property()
36
+ ], HelixHeadlessMapRoute.prototype, "description", void 0);
37
+ __decorate([
38
+ property()
39
+ ], HelixHeadlessMapRoute.prototype, "label", void 0);
40
+ __decorate([
41
+ property({ converter: pointsConverter })
42
+ ], HelixHeadlessMapRoute.prototype, "points", void 0);
43
+ __decorate([
44
+ property({ type: Number })
45
+ ], HelixHeadlessMapRoute.prototype, "progress", void 0);
46
+ HelixHeadlessMapRoute = __decorate([
47
+ customElement('helix-headless-map-route')
48
+ ], HelixHeadlessMapRoute);
49
+ export { HelixHeadlessMapRoute };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.104.1",
4
+ "version": "0.105.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,13 +46,13 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.104.1",
50
- "@fusion.dev/helix-icons": "0.104.1",
49
+ "@fusion.dev/helix-core": "0.105.0",
50
+ "@fusion.dev/helix-icons": "0.105.0",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@fusion.dev/helix-cem": "0.104.1",
55
- "@fusion.dev/helix-testing": "0.104.1",
54
+ "@fusion.dev/helix-cem": "0.105.0",
55
+ "@fusion.dev/helix-testing": "0.105.0",
56
56
  "@fusion.dev/scripts": "0.0.0-dev",
57
57
  "@happy-dom/global-registrator": "^20.0.0",
58
58
  "@lit-labs/ssr": "^4.1.0",