@fusion.dev/helix-headless 0.115.0 → 0.117.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.
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ export * from './empty/index.js';
38
38
  export * from './file-input/index.js';
39
39
  export * from './filter-input/index.js';
40
40
  export * from './format-date/index.js';
41
+ export * from './graph/index.js';
41
42
  export * from './heatmap/index.js';
42
43
  export * from './icon/index.js';
43
44
  export * from './icon-button/index.js';
@@ -81,6 +81,8 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
81
81
  * @csspart face-north - The compass: its needle points north as the map turns, and pressing it turns the map to face north.
82
82
  * @csspart footer - The strip below the map.
83
83
  * @csspart fullscreen - The button that makes the map fill the screen.
84
+ * @csspart gesture-hint - Words over the map for a moment when it lets a gesture pass to the page, such as how to zoom it.
85
+ * @csspart gesture-status - The line shown while a map with `gestures="activate"` has the gestures.
84
86
  * @csspart header - The strip above the map.
85
87
  * @csspart layer - One entry in the list of the map's layers.
86
88
  * @csspart layer-choice - One layer's tile in the layers menu, pressed while the layer is shown.
@@ -113,8 +115,14 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
113
115
  * @cssstate fullscreen - The map fills the screen.
114
116
  * @cssstate idle - The user has been still long enough for the header and footer to fade.
115
117
  * @cssstate ready - The map has been drawn whole, after `helix-map-ready`.
118
+ * @cssstate using - A map with `gestures="activate"` has been pressed and has the gestures.
116
119
  */
117
120
  export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
121
+ /**
122
+ * The hint shown when a map waiting to be pressed is scrolled or swiped
123
+ * over, with `gestures="activate"`.
124
+ */
125
+ activateHintLabel: string;
118
126
  /**
119
127
  * The map's rotation, in degrees clockwise from north. `connectMap` keeps it
120
128
  * current, and the Face north button's needle turns with it.
@@ -129,6 +137,21 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
129
137
  faceNorthLabel: string;
130
138
  /** What screen readers hear when the map is tilted back to looking straight down. */
131
139
  flatLabel: string;
140
+ /**
141
+ * Which gestures the map takes from the page.
142
+ *
143
+ * - `always`, which unset is: the wheel zooms the map and one finger pans
144
+ * it, for a map that is the page, such as a full-screen view.
145
+ * - `cooperative`: the wheel and one finger scroll the page, and the map
146
+ * takes Ctrl or ⌘ with the wheel, and two fingers. The frame says so
147
+ * when a gesture passes to the page. Best for a map embedded in a page.
148
+ * - `activate`: the map takes nothing until it is pressed, and gives it
149
+ * back on Escape, when the pointer leaves it, or on a press elsewhere.
150
+ *
151
+ * The keyboard and the frame's buttons work in every mode. The renderer is
152
+ * told through the connector, so MapLibre and Leaflet behave alike.
153
+ */
154
+ gestures?: 'activate' | 'always' | 'cooperative';
132
155
  /** The Hybrid map type's name. */
133
156
  hybridLabel: string;
134
157
  /**
@@ -190,6 +213,16 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
190
213
  * top of the map faces, clockwise from north.
191
214
  */
192
215
  turnedLabel: string;
216
+ /** The hint shown when one finger swipes over a map with `gestures="cooperative"`. */
217
+ twoFingerHintLabel: string;
218
+ /** The line shown while a map with `gestures="activate"` has the gestures. */
219
+ usingLabel: string;
220
+ /**
221
+ * The hint shown when the wheel scrolls past a map with
222
+ * `gestures="cooperative"`, with `{key}`, which is ⌘ on a Mac and Ctrl
223
+ * elsewhere, as the renderer reads it.
224
+ */
225
+ wheelHintLabel: string;
193
226
  /**
194
227
  * What screen readers hear after a zoom. `{level}` is the zoom level.
195
228
  */
@@ -315,6 +348,51 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
315
348
  */
316
349
  private readonly handleMarkerKeydown;
317
350
  private readonly handleMarkerKeyup;
351
+ /** The hint on the map while one shows, put there by a gesture the map let pass. */
352
+ private hint;
353
+ /** When the hint goes. */
354
+ private hinting;
355
+ /**
356
+ * Where the gesture was, in the map's box, which the hint is shown at:
357
+ * where the reader is looking, and on the map wherever a page laid its own
358
+ * content over the rest of it.
359
+ */
360
+ private hintAt;
361
+ /** Whether a map with `gestures="activate"` has been pressed, and has the gestures. */
362
+ private taken;
363
+ /**
364
+ * @internal Which gestures the renderer takes, which `connectMap` tells it:
365
+ * all of them, the cooperative set, or none, while an `activate` map waits
366
+ * to be pressed.
367
+ */
368
+ get gestureMode(): 'cooperative' | 'free' | 'off';
369
+ /** Shows a hint over the map for a moment, where the gesture was. */
370
+ private showHint;
371
+ /**
372
+ * A wheel the map let pass to the page, which is said: a reader scrolling
373
+ * the page past a map that did not zoom is told why, and how to zoom it.
374
+ * **Ctrl or ⌘ with the wheel is the map's**, and is what a trackpad's pinch
375
+ * arrives as.
376
+ */
377
+ private readonly handleWheel;
378
+ /** One finger moving over the map, which scrolls the page, and is said. */
379
+ private readonly handleTouchMove;
380
+ /** A press on the map itself, which hands a waiting map the gestures. */
381
+ private readonly handleTake;
382
+ /** Escape, which gives the page its scroll back. */
383
+ private readonly handleGiveBackKey;
384
+ /** A press anywhere else on the page, which gives it its scroll back. */
385
+ private readonly handleGiveBackOutside;
386
+ /** The window losing focus, as a reader switching away does. */
387
+ private readonly handleGiveBackBlur;
388
+ /**
389
+ * The pointer leaving the frame. **Heard on the frame and not the map's
390
+ * canvas**, which a pointer resting on a marker has left, since markers
391
+ * stand outside it. **A mouse or a pen and never a finger**, whose
392
+ * `pointerleave` is the finger lifting: a tap took the map and gave it back
393
+ * in one.
394
+ */
395
+ private readonly handleLeave;
318
396
  /** A marker added or taken away is a walk of a different length. */
319
397
  private readonly handleSlotChange;
320
398
  connectedCallback(): void;
@@ -37,10 +37,12 @@ const TILT_STEP = 10;
37
37
  const TILTED = 50;
38
38
  const placed = (marker) => Number.isFinite(marker.lat) && Number.isFinite(marker.lng);
39
39
  const ASK_LAPSES = 3000;
40
+ const HINT_SHOWS = 1600;
40
41
  const SETTLES = 400;
41
42
  let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement) {
42
43
  constructor() {
43
44
  super(...arguments);
45
+ this.activateHintLabel = 'Press the map to use it';
44
46
  this.bearing = 0;
45
47
  this.cluster = false;
46
48
  this.faceNorthLabel = 'Face north';
@@ -63,6 +65,9 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
63
65
  this.tiltedLabel = 'Tilted {degrees} degrees';
64
66
  this.tiltLabel = 'Show in 3D';
65
67
  this.turnedLabel = 'Facing {degrees} degrees';
68
+ this.twoFingerHintLabel = 'Use two fingers to move the map';
69
+ this.usingLabel = 'Using the map. Press Escape to scroll the page again.';
70
+ this.wheelHintLabel = 'Use {key} and scroll to zoom the map';
66
71
  this.zoomedLabel = 'Zoom level {level}';
67
72
  this.said = '';
68
73
  this.asked = undefined;
@@ -131,6 +136,61 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
131
136
  this.activate(marker);
132
137
  }
133
138
  };
139
+ this.hint = '';
140
+ this.taken = false;
141
+ this.handleWheel = (event) => {
142
+ if (this.gestures === 'cooperative' && !(event.ctrlKey || event.metaKey)) {
143
+ const mac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac');
144
+ this.showHint(fillTemplate(this.wheelHintLabel, { key: mac ? '⌘' : 'Ctrl' }), event);
145
+ }
146
+ else if (this.gestures === 'activate' && !this.taken) {
147
+ this.showHint(this.activateHintLabel, event);
148
+ }
149
+ };
150
+ this.handleTouchMove = (event) => {
151
+ const finger = event.touches[0];
152
+ if (event.touches.length !== 1) {
153
+ return;
154
+ }
155
+ if (this.gestures === 'cooperative') {
156
+ this.showHint(this.twoFingerHintLabel, finger);
157
+ }
158
+ else if (this.gestures === 'activate' && !this.taken) {
159
+ this.showHint(this.activateHintLabel, finger);
160
+ }
161
+ };
162
+ this.handleTake = (event) => {
163
+ const viewport = this.partNamed('viewport');
164
+ if (this.gestures !== 'activate' || this.taken || !viewport) {
165
+ return;
166
+ }
167
+ const path = event.composedPath();
168
+ const onMap = path.includes(viewport) ||
169
+ path.some((one) => one instanceof Element && one.parentElement === this && one.slot === '');
170
+ if (onMap) {
171
+ this.taken = true;
172
+ this.hint = '';
173
+ }
174
+ };
175
+ this.handleGiveBackKey = (event) => {
176
+ if (event.key === 'Escape' && this.taken) {
177
+ this.taken = false;
178
+ }
179
+ };
180
+ this.handleGiveBackOutside = (event) => {
181
+ if (!event.composedPath().includes(this)) {
182
+ this.taken = false;
183
+ }
184
+ };
185
+ this.handleGiveBackBlur = () => {
186
+ this.taken = false;
187
+ };
188
+ this.handleLeave = (event) => {
189
+ this.idle.leave();
190
+ if (event.pointerType !== 'touch') {
191
+ this.taken = false;
192
+ }
193
+ };
134
194
  this.handleSlotChange = () => {
135
195
  this.requestUpdate();
136
196
  };
@@ -243,6 +303,24 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
243
303
  }
244
304
  this.dispatchEvent(new HelixActivateEvent({ value: marker.name || marker.label || '' }));
245
305
  }
306
+ get gestureMode() {
307
+ if (this.gestures === 'cooperative') {
308
+ return 'cooperative';
309
+ }
310
+ return this.gestures === 'activate' && !this.taken ? 'off' : 'free';
311
+ }
312
+ showHint(words, at) {
313
+ const box = this.partNamed('viewport')?.getBoundingClientRect();
314
+ this.hintAt =
315
+ at && box && box.width > 0 && box.height > 0
316
+ ? { x: (at.clientX - box.left) / box.width, y: (at.clientY - box.top) / box.height }
317
+ : undefined;
318
+ this.hint = words;
319
+ clearTimeout(this.hinting);
320
+ this.hinting = setTimeout(() => {
321
+ this.hint = '';
322
+ }, HINT_SHOWS);
323
+ }
246
324
  connectedCallback() {
247
325
  super.connectedCallback();
248
326
  this.addEventListener(HelixMapPartChangeEvent.eventName, this.handlePartChange);
@@ -250,6 +328,10 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
250
328
  this.addEventListener('focusin', this.handleFocusIn);
251
329
  this.addEventListener('keydown', this.handleMarkerKeydown);
252
330
  this.addEventListener('keyup', this.handleMarkerKeyup);
331
+ this.addEventListener('keydown', this.handleGiveBackKey);
332
+ this.addEventListener('pointerdown', this.handleTake);
333
+ this.addEventListener('touchmove', this.handleTouchMove, { passive: true });
334
+ this.addEventListener('wheel', this.handleWheel, { passive: true });
253
335
  this.hiding = new MutationObserver(() => this.requestUpdate());
254
336
  this.hiding.observe(this, { attributeFilter: ['hidden'], attributes: true, subtree: true });
255
337
  }
@@ -264,6 +346,14 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
264
346
  this.removeEventListener('focusin', this.handleFocusIn);
265
347
  this.removeEventListener('keydown', this.handleMarkerKeydown);
266
348
  this.removeEventListener('keyup', this.handleMarkerKeyup);
349
+ this.removeEventListener('keydown', this.handleGiveBackKey);
350
+ this.removeEventListener('pointerdown', this.handleTake);
351
+ this.removeEventListener('touchmove', this.handleTouchMove);
352
+ this.removeEventListener('wheel', this.handleWheel);
353
+ clearTimeout(this.hinting);
354
+ this.taken = false;
355
+ document.removeEventListener('pointerdown', this.handleGiveBackOutside);
356
+ window.removeEventListener('blur', this.handleGiveBackBlur);
267
357
  }
268
358
  firstUpdated(changed) {
269
359
  super.firstUpdated(changed);
@@ -274,6 +364,20 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
274
364
  if (changed.has('mapType')) {
275
365
  this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'layer' }));
276
366
  }
367
+ if (changed.has('gestures') || changed.has('taken')) {
368
+ if (this.gestures !== 'activate' && this.taken) {
369
+ this.taken = false;
370
+ }
371
+ if (this.taken) {
372
+ document.addEventListener('pointerdown', this.handleGiveBackOutside);
373
+ window.addEventListener('blur', this.handleGiveBackBlur);
374
+ }
375
+ else {
376
+ document.removeEventListener('pointerdown', this.handleGiveBackOutside);
377
+ window.removeEventListener('blur', this.handleGiveBackBlur);
378
+ }
379
+ this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'gestures' }));
380
+ }
277
381
  if (changed.has('layersOpen')) {
278
382
  if (this.layersOpen) {
279
383
  document.addEventListener('pointerdown', this.handleOutside);
@@ -515,6 +619,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
515
619
  hostAria(this.internals, { label: this.label });
516
620
  this.toggleState('fullscreen', this.filled);
517
621
  this.toggleState('idle', this.resting);
622
+ this.toggleState('using', this.taken);
518
623
  }
519
624
  renderLayers() {
520
625
  const layers = this.layers.filter((layer) => this.shown(layer));
@@ -534,7 +639,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
534
639
  tabindex="0"
535
640
  @focusin=${this.idle.stir}
536
641
  @keydown=${this.handleKeydown}
537
- @pointerleave=${this.idle.leave}
642
+ @pointerleave=${this.handleLeave}
538
643
  @pointermove=${this.idle.stir}
539
644
  >
540
645
  <div part="header">
@@ -542,6 +647,13 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
542
647
  </div>
543
648
  <div part="viewport">
544
649
  <slot @slotchange=${this.handleSlotChange}></slot>
650
+ <div
651
+ part="gesture-hint"
652
+ aria-hidden="true"
653
+ ?hidden=${this.hint === ''}
654
+ style=${this.hintAt ? `--_map-hint-x: ${(this.hintAt.x * 100).toFixed(2)}%; --_map-hint-y: ${(this.hintAt.y * 100).toFixed(2)}%` : nothing}
655
+ >${this.hint}</div>
656
+ <div part="gesture-status" aria-hidden="true" ?hidden=${!this.taken}>${this.usingLabel}</div>
545
657
  </div>
546
658
  ${this.renderLayers()}
547
659
  ${this.renderControls()}
@@ -555,6 +667,9 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
555
667
  </div>`;
556
668
  }
557
669
  };
670
+ __decorate([
671
+ property({ attribute: 'activate-hint-label' })
672
+ ], HelixHeadlessMap.prototype, "activateHintLabel", void 0);
558
673
  __decorate([
559
674
  property({ type: Number })
560
675
  ], HelixHeadlessMap.prototype, "bearing", void 0);
@@ -567,6 +682,9 @@ __decorate([
567
682
  __decorate([
568
683
  property({ attribute: 'flat-label' })
569
684
  ], HelixHeadlessMap.prototype, "flatLabel", void 0);
685
+ __decorate([
686
+ property()
687
+ ], HelixHeadlessMap.prototype, "gestures", void 0);
570
688
  __decorate([
571
689
  property({ attribute: 'hybrid-label' })
572
690
  ], HelixHeadlessMap.prototype, "hybridLabel", void 0);
@@ -624,6 +742,15 @@ __decorate([
624
742
  __decorate([
625
743
  property({ attribute: 'turned-label' })
626
744
  ], HelixHeadlessMap.prototype, "turnedLabel", void 0);
745
+ __decorate([
746
+ property({ attribute: 'two-finger-hint-label' })
747
+ ], HelixHeadlessMap.prototype, "twoFingerHintLabel", void 0);
748
+ __decorate([
749
+ property({ attribute: 'using-label' })
750
+ ], HelixHeadlessMap.prototype, "usingLabel", void 0);
751
+ __decorate([
752
+ property({ attribute: 'wheel-hint-label' })
753
+ ], HelixHeadlessMap.prototype, "wheelHintLabel", void 0);
627
754
  __decorate([
628
755
  property({ attribute: 'zoomed-label' })
629
756
  ], HelixHeadlessMap.prototype, "zoomedLabel", void 0);
@@ -639,6 +766,15 @@ __decorate([
639
766
  __decorate([
640
767
  state()
641
768
  ], HelixHeadlessMap.prototype, "stop", void 0);
769
+ __decorate([
770
+ state()
771
+ ], HelixHeadlessMap.prototype, "hint", void 0);
772
+ __decorate([
773
+ state()
774
+ ], HelixHeadlessMap.prototype, "hintAt", void 0);
775
+ __decorate([
776
+ state()
777
+ ], HelixHeadlessMap.prototype, "taken", void 0);
642
778
  HelixHeadlessMap = __decorate([
643
779
  customElement('helix-headless-map')
644
780
  ], HelixHeadlessMap);
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.115.0",
4
+ "version": "0.117.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.115.0",
50
- "@fusion.dev/helix-icons": "0.115.0",
49
+ "@fusion.dev/helix-core": "0.117.0",
50
+ "@fusion.dev/helix-icons": "0.117.0",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@fusion.dev/helix-cem": "0.115.0",
55
- "@fusion.dev/helix-testing": "0.115.0",
54
+ "@fusion.dev/helix-cem": "0.117.0",
55
+ "@fusion.dev/helix-testing": "0.117.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",