@fusion.dev/helix-docs 0.176.1 → 0.177.1

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.
@@ -0,0 +1,141 @@
1
+ ---
2
+ description: Add a board of cards and connections that people can move, connect, and read with any input, and save it as JSON Canvas.
3
+ section: Essentials
4
+ title: Canvases
5
+ ---
6
+
7
+ # Canvases
8
+
9
+ Helix Canvas draws a board of cards and the connections between them. People
10
+ pan and zoom the board, move and resize cards, draw connections, and walk the
11
+ board in reading order from the keyboard. The board reads and writes
12
+ [JSON Canvas](https://jsoncanvas.org), an open format.
13
+
14
+ ## Add a board
15
+
16
+ Write each card as a `helix-canvas-node` and each connection as a
17
+ `helix-canvas-edge`. A card's `x`, `y`, `width`, and `height` are in the
18
+ document's own units, and a connection names its two cards by `name`:
19
+
20
+ ```html
21
+ <helix-canvas label="Launch plan">
22
+ <helix-canvas-node name="draft" x="0" y="0" width="160" height="64">Draft the email</helix-canvas-node>
23
+ <helix-canvas-node name="ship" tone="success" x="256" y="0" width="160" height="64">Publish the release</helix-canvas-node>
24
+ <helix-canvas-edge from="draft" to="ship"></helix-canvas-edge>
25
+ </helix-canvas>
26
+ ```
27
+
28
+ ```js
29
+ import '@fusion.dev/helix/canvas'
30
+ ```
31
+
32
+ Give the board a `label` that says what it holds. A screen reader announces it
33
+ as "Launch plan, canvas".
34
+
35
+ ## Load and save a document
36
+
37
+ `fromJsonCanvas` turns a JSON Canvas document into the props of the elements.
38
+ What a card shows is yours to render, so this example writes each text card's
39
+ text:
40
+
41
+ ```js
42
+ import { fromJsonCanvas } from '@fusion.dev/helix/canvas'
43
+
44
+ const board = document.querySelector('helix-canvas')
45
+ const { nodes, edges } = fromJsonCanvas(saved)
46
+ for (const node of nodes) {
47
+ const card = document.createElement('helix-canvas-node')
48
+ const { group, height, label, name, tone, width, x, y } = node
49
+ Object.assign(card, { group, height, label, name, tone, width, x, y })
50
+ card.textContent = node.text ?? ''
51
+ board.append(card)
52
+ }
53
+ for (const edge of edges) {
54
+ const line = document.createElement('helix-canvas-edge')
55
+ const { from, fromSide, label, name, to, toSide, tone } = edge
56
+ Object.assign(line, { from, fromSide, label, name, to, toSide, tone })
57
+ board.append(line)
58
+ }
59
+ ```
60
+
61
+ `board.toJsonCanvas()` writes the board back out as a JSON Canvas document.
62
+
63
+ ## Keep the board in your state
64
+
65
+ The board moves a card itself and sends `helix-move` when the move is finished.
66
+ To keep positions in your own state, cancel `helix-before-move` and write the
67
+ positions from its detail:
68
+
69
+ ```js
70
+ board.addEventListener('helix-before-move', (event) => {
71
+ event.preventDefault()
72
+ for (const { name, x, y } of event.detail.nodes) {
73
+ const card = board.querySelector(`[name="${name}"]`)
74
+ card.x = x
75
+ card.y = y
76
+ }
77
+ })
78
+ ```
79
+
80
+ Connecting and removing are requests that your page applies, because a new
81
+ connection is a new element:
82
+
83
+ - `helix-connect` names two cards and their sides. Add a `helix-canvas-edge`
84
+ that joins them.
85
+ - `helix-remove` names the cards and connections to remove, including every
86
+ connection that removing a card would leave without an end. Remove those
87
+ elements.
88
+ - `helix-canvas-edit` asks you to edit a card. Open your own editor in it.
89
+
90
+ Set `readonly` on the board to keep every card in place, or on one card or
91
+ connection to keep only that one.
92
+
93
+ ## Keyboard and screen readers
94
+
95
+ The board is one tab stop. A card holds it, and Tab leaves the board.
96
+
97
+ | Keys | On | What they do |
98
+ |---|---|---|
99
+ | Arrow keys | A card or a connection | Move through the cards in reading order, each card's connections straight after it, and select what they land on |
100
+ | Home, End | A card or a connection | Move to the first or the last stop |
101
+ | Control or Command with an arrow key, Home, or End | A card or a connection | Move without changing the selection |
102
+ | Space | A card or a connection | Add it to the selection or take it out |
103
+ | Control or Command with A | A card or a connection | Select everything |
104
+ | Shift with an arrow key | A card | Move the card by 16 units, or to the next multiple of `snap` |
105
+ | Alt with an arrow key | A card | Resize the card from its bottom right corner |
106
+ | Enter or F2 | A card | Ask to edit it, with `helix-canvas-edit`, or edit a group's name |
107
+ | Enter | A connection | Move to the card it arrives at |
108
+ | F2 | A connection | Edit its label |
109
+ | C, then Enter on another card | A card | Ask to connect the two, with `helix-connect` |
110
+ | C | A connection | Pick up its end, to move it to another card with Enter |
111
+ | Delete or Backspace | A card or a connection | Ask to remove what is selected, with `helix-remove` |
112
+ | Control or Command with C, V | The board | Copy the selected cards as JSON Canvas, and paste a document |
113
+ | Control or Command with Z | The board | Undo the last move or resize. With Shift, or Control with Y, make it again |
114
+ | Plus, minus | The board, a card, or a connection | Zoom in or out |
115
+ | Escape | A card or a connection | Stop connecting, or go out to the board |
116
+ | Arrow keys | The board, after Escape | Pan the board |
117
+
118
+ Every drag also works with clicks. Click a selected card's move handle, a
119
+ corner, a card's connection point, or the end of a selected connection to pick
120
+ it up, then click where it goes. Escape puts it back.
121
+
122
+ Screen readers meet these roles and names:
123
+
124
+ | Part | Role | Named by |
125
+ |---|---|---|
126
+ | The board | `group`, described as "canvas" | `label` |
127
+ | A card | `article` | Its content, or its `label` |
128
+ | A group card | `group` | Its `label` |
129
+ | A connection | `button` | "From Draft to Ship", from `connection-label` |
130
+ | The controls | `group` | "Board controls", from `controls-label` |
131
+ | What changed | `status` | The board's own words |
132
+
133
+ A card is also described by its connections, such as "leads to Ship", and by
134
+ whether it is selected and what its tone means, from `tone-labels`. After a key
135
+ moves, resizes, selects, copies, pastes, undoes, or zooms, screen readers hear
136
+ what changed, such as "Ship moved to 16, 200". Each sentence is a `-label`
137
+ property, so you can write it in your page's language, or register the words
138
+ for a language with `registerWords` (see [Localization](./localization)).
139
+
140
+ When your page removes the card that has focus, focus moves to the next stop,
141
+ or to the one before when it was the last.
@@ -430,9 +430,16 @@ leaves removing it to your framework.
430
430
  whose name starts with it, and Enter or Space selects a note.
431
431
  - In the search field, Down moves into the list, and Escape empties the search.
432
432
  With `no-panel`, Escape on an empty search closes the list.
433
+ - The legend is a toolbar and one tab stop. Arrow keys, Home, and End move
434
+ between its groups, typing a letter moves to the next group whose name
435
+ starts with it, and Space or Enter shows or hides a group.
433
436
  - Screen readers read the selected note first, then how each other note links
434
437
  to it, such as "Linked from Roadmap". They hear the count when a search or
435
438
  the legend changes it.
439
+ - When you select a note, in the list or in the drawing, screen readers hear
440
+ its name and how many notes the list holds, such as "Roadmap, selected. 4
441
+ notes", from `selection-label`. A selection your page writes to `active` is
442
+ not said.
436
443
  - After the arrow keys, plus, minus, or the buttons move the drawing, screen
437
444
  readers hear which way it went, such as "Moved right" or "Zoomed in".
438
445
  `connectGraph` tells the frame once the drawing has moved. If you answer
@@ -441,6 +448,7 @@ leaves removing it to your framework.
441
448
  - When you change `nodes` or `links`, the list keeps the note the keys are on.
442
449
  When that note is removed, the keys move to the note that took its row.
443
450
  - Change the words with `search-label`, `count-label`, `selected-label`,
451
+ `selection-label`,
444
452
  `links-to-label`, `linked-from-label`, `linked-both-label`,
445
453
  `moved-up-label`, `moved-down-label`, `moved-left-label`,
446
454
  `moved-right-label`, `zoomed-in-label`, and `zoomed-out-label`, or register
package/content/maps.mdx CHANGED
@@ -642,25 +642,166 @@ compass sets `--map-needle-turn` to the angle that points north, so an icon in
642
642
 
643
643
  ## Write a driver
644
644
 
645
- To use another renderer, write a driver: an object with two methods.
645
+ To use another renderer, write a driver: an object that moves the renderer's
646
+ view when the frame asks, and tells the frame when the view moved. `connectMap`
647
+ calls it. `mapLibreDriver` and `leafletDriver` are drivers, and the `MapDriver`
648
+ type in `@fusion.dev/helix-map` describes every method.
646
649
 
647
- - `panBy(x, y, motion)` moves the map by a number of pixels.
648
- - `zoomBy(direction, motion)` zooms in for `1` and out for `-1`.
650
+ ### Units
649
651
 
650
- `motion` is your theme's `{ duration, easing }`, or `null` to move without
651
- animation. Each optional method turns on a feature:
652
-
653
- | Method | Turns on |
652
+ | Quantity | Unit |
654
653
  |---|---|
655
- | `project`, `onView` | Placing markers and positions |
656
- | `show` | Bringing a focused marker into view |
657
- | `fit` | `connection.fit()` and zooming in to clusters |
658
- | `layers` | Drawing routes, areas, and points |
659
- | `credits` | The map credit in the frame's footer |
660
- | `silenceKeyboard`, `focusTarget`, `replaceZoom` | Using the frame's keys and zoom buttons |
661
- | `bearing` | Arrows that stay correct when the map is rotated, and the Face north arrow |
662
- | `rotate`, `tilt` | The rotate, Face north, and tilt buttons and keys |
663
- | `zoom` | Saying the zoom level after a zoom |
664
-
665
- Without `project`, markers stay hidden. To animate a move your page makes
666
- itself, read the same motion with `motionOf(element, pace)`.
654
+ | A place | `{ lat, lng }` in degrees |
655
+ | A position on screen | CSS pixels from the top left of the renderer's box, right and down positive |
656
+ | Bearing | Degrees clockwise from north |
657
+ | Tilt | Degrees from looking straight down, `0` being flat |
658
+ | Zoom | The renderer's own levels |
659
+ | Motion | `{ duration, easing }`, where `duration` is in milliseconds and `easing(t)` maps 0 to 1 onto 0 to 1, or `null` |
660
+
661
+ `motion` comes from your theme's motion tokens. It is `null` when the user has
662
+ asked for reduced motion, and the driver then moves at once.
663
+
664
+ ### Required methods
665
+
666
+ - `panBy(x, y, motion)` moves the view by `x` and `y` CSS pixels. The frame
667
+ asks for a fraction of its own box, so an arrow key moves the map by the same
668
+ share of the screen at every zoom. On a right-to-left page, `connectMap`
669
+ turns the frame's start and end into left and right before it calls the
670
+ driver.
671
+ - `zoomBy(direction, motion)` zooms one step in, for `1`, or out, for `-1`. A
672
+ step is whatever the renderer calls one, usually one level.
673
+
674
+ ### Optional methods
675
+
676
+ Each optional method turns on a feature. Without it, the feature is off and
677
+ nothing fails.
678
+
679
+ | Method | When `connectMap` calls it | What it must do | Turns on |
680
+ |---|---|---|---|
681
+ | `onView(moved)` | Once, at connect | Call `moved` on every frame the view moves, and return a function that stops | Markers that follow the map, and the announcements after a move |
682
+ | `project(place)` | On every view change, for each marker | Return where `place` falls, in CSS pixels | Placing markers and positions. Without it, markers stay hidden |
683
+ | `zoom()` | After each view change | Return the zoom | Saying "Zoom level 14" after a zoom |
684
+ | `bearing()`, `pitch()` | After each view change | Return the bearing and the tilt | The compass needle, the 3D button's pressed state, and arrows that stay correct on a turned map |
685
+ | `rotate({ by, to }, motion)` | On the rotate buttons, Face north, and Shift with Left or Right | Turn by `by` degrees, or to the bearing `to` | Rotating |
686
+ | `tilt({ by, to }, motion)` | On the 3D button, and Shift with Up or Down | Tilt by `by` degrees, or to `to` | Tilting |
687
+ | `show(place, motion)` | When the keyboard reaches a marker that is off screen | Bring `place` to the middle of the view, at `place.zoom` where it has one | Walking markers off screen |
688
+ | `fit(bounds, padding, motion)` | On `connection.fit()`, and on a press on a cluster | Frame the `northEast` and `southWest` corners, inset by `padding` pixels | `connection.fit()` and zooming in to clusters |
689
+ | `silenceKeyboard()` | Once, at connect | Turn off the renderer's own keys, and return a function that turns them back on | One set of keys |
690
+ | `focusTarget()` | Once, at connect | Return the element the renderer made focusable | One tab stop. `connectMap` sets its `tabindex` to `-1` and puts it back on disconnect |
691
+ | `replaceZoom()` | Once the frame has drawn its zoom buttons | Remove the renderer's zoom buttons, and return a function that puts them back | One set of zoom buttons |
692
+ | `credits(changed)` | Once, at connect | Return `read()`, the credits as markup, and `stop()`; call `changed` when they change | The map credit in the frame's footer |
693
+ | `ready(done)` | Once, at connect | Call `done` when the view is drawn whole, and return a function that stops waiting | `helix-map-ready` and `:state(ready)` |
694
+ | `gestures(mode)` | When the frame's `gestures` changes | Take the wheel and touches as `free`, `cooperative`, or `off` says | Sharing the scroll with the page |
695
+ | `layers(read, events)` | Once, at connect | Draw `read()`, and draw it again on `update()` | Routes, areas, points, and tile layers |
696
+ | `labels(shown)` | When the map type changes | Show or hide the renderer's own roads and names | The Satellite map type |
697
+ | `previews(ask)` | When the layers menu opens after the view moved | Resolve to an image URL for each map type | Pictures in the layers menu |
698
+ | `behind(place)` | With `project` | Return `true` for a place on the far side of a globe | Hiding markers behind a globe |
699
+
700
+ `connection.disconnect()` calls every function a method returned, gives the
701
+ renderer its gestures back with `gestures('free')`, takes the markers off their
702
+ places, puts the focus target's `tabindex` back as it was, and stops listening
703
+ to the frame. Call it before you remove the renderer.
704
+
705
+ ### How the frame says a move
706
+
707
+ The frame's keys and buttons only ask. The frame says "Moved left" or "Zoom
708
+ level 14" after the driver calls the `moved` it gave to `onView`, and once the
709
+ view has been still for a moment. A driver without `onView` never makes the
710
+ frame speak, so the frame never describes a move that did not happen.
711
+
712
+ ### Example
713
+
714
+ This driver shows a plan drawn as one image, such as a floor plan, whose edges
715
+ are at known latitudes and longitudes. It moves the image with a CSS transform,
716
+ places markers, and animates over the page's motion. The same driver runs in
717
+ our specs, so this example stays correct.
718
+
719
+ ```js
720
+ import { connectMap } from '@fusion.dev/helix-map'
721
+
722
+ const planDriver = (image, plan) => {
723
+ const view = { scale: 1, x: 0, y: 0 }
724
+ const listeners = new Set()
725
+ let moving = 0
726
+
727
+ const draw = () => {
728
+ image.style.transformOrigin = '0 0'
729
+ image.style.transform = `translate(${view.x}px, ${view.y}px) scale(${view.scale})`
730
+ for (const moved of listeners) {
731
+ moved()
732
+ }
733
+ }
734
+
735
+ // Moves the view to `next` over the page's motion, or at once where it is null.
736
+ const go = (next, motion) => {
737
+ const run = ++moving
738
+ if (!motion || motion.duration <= 0) {
739
+ Object.assign(view, next)
740
+ draw()
741
+ return
742
+ }
743
+ const from = { ...view }
744
+ const start = performance.now()
745
+ const step = (now) => {
746
+ if (run !== moving) {
747
+ return
748
+ }
749
+ const t = Math.min(1, (now - start) / motion.duration)
750
+ const k = motion.easing(t)
751
+ view.x = from.x + (next.x - from.x) * k
752
+ view.y = from.y + (next.y - from.y) * k
753
+ view.scale = from.scale + (next.scale - from.scale) * k
754
+ draw()
755
+ if (t < 1) {
756
+ requestAnimationFrame(step)
757
+ }
758
+ }
759
+ requestAnimationFrame(step)
760
+ }
761
+
762
+ return {
763
+ onView: (moved) => {
764
+ listeners.add(moved)
765
+ return () => listeners.delete(moved)
766
+ },
767
+ panBy: (x, y, motion) => go({ ...view, x: view.x - x, y: view.y - y }, motion),
768
+ project: ({ lat, lng }) => ({
769
+ x: view.x + ((lng - plan.west) / (plan.east - plan.west)) * plan.width * view.scale,
770
+ y: view.y + ((plan.north - lat) / (plan.north - plan.south)) * plan.height * view.scale,
771
+ }),
772
+ zoom: () => Math.log2(view.scale),
773
+ zoomBy: (direction, motion) => {
774
+ const box = image.parentElement.getBoundingClientRect()
775
+ const k = direction > 0 ? 2 : 0.5
776
+ const [cx, cy] = [box.width / 2, box.height / 2]
777
+ go({ scale: view.scale * k, x: cx - (cx - view.x) * k, y: cy - (cy - view.y) * k }, motion)
778
+ },
779
+ }
780
+ }
781
+
782
+ const frame = document.querySelector('helix-map')
783
+ const connection = connectMap(
784
+ frame,
785
+ planDriver(frame.querySelector('img'), {
786
+ east: 0.004,
787
+ height: 600,
788
+ north: 51.502,
789
+ south: 51.5,
790
+ west: 0,
791
+ width: 800,
792
+ }),
793
+ )
794
+ ```
795
+
796
+ `width` and `height` are the image's size in CSS pixels at zoom `0`. To animate
797
+ a move your page makes itself, read the same motion with
798
+ `motionOf(element, pace)`.
799
+
800
+ ### Test a driver
801
+
802
+ Our specs test each driver twice. A stand-in renderer records what the driver
803
+ asks it, which runs anywhere. Then the browser specs connect a frame to the real
804
+ MapLibre and Leaflet with a style that loads nothing, press the frame's keys and
805
+ buttons, and check that the view moved and that the frame said so. Test your
806
+ driver the same way: connect it to a frame, press a key on the frame's `region`
807
+ part, and wait for the `live` part to say the move.