react-msaview 9.1.1 → 9.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 (89) hide show
  1. package/bundle/index.js +79 -79
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/DragHandle.js +2 -1
  4. package/dist/components/DragHandle.js.map +1 -1
  5. package/dist/components/MSAViewer.d.ts +31 -19
  6. package/dist/components/MSAViewer.js +2 -1
  7. package/dist/components/MSAViewer.js.map +1 -1
  8. package/dist/components/Track.js +2 -5
  9. package/dist/components/Track.js.map +1 -1
  10. package/dist/components/VerticalScrollbar.js +1 -1
  11. package/dist/components/VerticalScrollbar.js.map +1 -1
  12. package/dist/components/header/Header.js +2 -1
  13. package/dist/components/header/Header.js.map +1 -1
  14. package/dist/components/minimap/Minimap.js +1 -1
  15. package/dist/components/minimap/Minimap.js.map +1 -1
  16. package/dist/components/msa/MSACanvas.js +16 -27
  17. package/dist/components/msa/MSACanvas.js.map +1 -1
  18. package/dist/components/rowpanels/RowPanels.js +8 -6
  19. package/dist/components/rowpanels/RowPanels.js.map +1 -1
  20. package/dist/components/settings/SettingsPanel.js +2 -1
  21. package/dist/components/settings/SettingsPanel.js.map +1 -1
  22. package/dist/components/settings/TrackList.js +8 -5
  23. package/dist/components/settings/TrackList.js.map +1 -1
  24. package/dist/components/tracks/TrackTooltipContent.js +2 -1
  25. package/dist/components/tracks/TrackTooltipContent.js.map +1 -1
  26. package/dist/components/tracks/drawTracks.js +13 -6
  27. package/dist/components/tracks/drawTracks.js.map +1 -1
  28. package/dist/components/tree/TreeCanvas.js +8 -6
  29. package/dist/components/tree/TreeCanvas.js.map +1 -1
  30. package/dist/constants.d.ts +1 -0
  31. package/dist/constants.js +2 -0
  32. package/dist/constants.js.map +1 -1
  33. package/dist/element.d.ts +11 -7
  34. package/dist/element.js +46 -8
  35. package/dist/element.js.map +1 -1
  36. package/dist/featurePanels.d.ts +0 -7
  37. package/dist/featurePanels.js +1 -16
  38. package/dist/featurePanels.js.map +1 -1
  39. package/dist/ggplotPalettes.js +12 -1
  40. package/dist/ggplotPalettes.js.map +1 -1
  41. package/dist/groupKeys.d.ts +6 -0
  42. package/dist/groupKeys.js +52 -0
  43. package/dist/groupKeys.js.map +1 -0
  44. package/dist/index.d.ts +1 -1
  45. package/dist/model.d.ts +40 -17
  46. package/dist/model.js +123 -54
  47. package/dist/model.js.map +1 -1
  48. package/dist/scales.d.ts +114 -13
  49. package/dist/scales.js +154 -19
  50. package/dist/scales.js.map +1 -1
  51. package/dist/svgTestUtil.d.ts +22 -12
  52. package/dist/types.d.ts +30 -28
  53. package/dist/useDragScroll.d.ts +4 -3
  54. package/dist/useDragScroll.js +9 -6
  55. package/dist/useDragScroll.js.map +1 -1
  56. package/dist/useWheelScroll.d.ts +26 -8
  57. package/dist/useWheelScroll.js +108 -71
  58. package/dist/useWheelScroll.js.map +1 -1
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/viewerSnapshot.js +2 -1
  62. package/dist/viewerSnapshot.js.map +1 -1
  63. package/package.json +3 -3
  64. package/src/components/DragHandle.tsx +2 -1
  65. package/src/components/MSAViewer.tsx +11 -8
  66. package/src/components/Track.tsx +2 -8
  67. package/src/components/VerticalScrollbar.tsx +1 -1
  68. package/src/components/header/Header.tsx +2 -1
  69. package/src/components/minimap/Minimap.tsx +1 -1
  70. package/src/components/msa/MSACanvas.tsx +14 -35
  71. package/src/components/rowpanels/RowPanels.tsx +8 -10
  72. package/src/components/settings/SettingsPanel.tsx +2 -1
  73. package/src/components/settings/TrackList.tsx +9 -4
  74. package/src/components/tracks/TrackTooltipContent.tsx +2 -1
  75. package/src/components/tracks/drawTracks.ts +15 -7
  76. package/src/components/tree/TreeCanvas.tsx +8 -10
  77. package/src/constants.ts +2 -0
  78. package/src/element.ts +53 -8
  79. package/src/featurePanels.ts +1 -25
  80. package/src/ggplotPalettes.ts +13 -1
  81. package/src/groupKeys.ts +54 -0
  82. package/src/index.ts +13 -2
  83. package/src/model.ts +174 -72
  84. package/src/scales.ts +268 -25
  85. package/src/types.ts +29 -26
  86. package/src/useDragScroll.ts +11 -8
  87. package/src/useWheelScroll.ts +124 -84
  88. package/src/version.ts +1 -1
  89. package/src/viewerSnapshot.ts +3 -1
@@ -2,24 +2,71 @@ import { useEffect, useRef, useState } from 'react'
2
2
 
3
3
  import { WheelZoomSmoother } from './wheelZoom.ts'
4
4
 
5
+ import type { MsaViewModel } from './model.ts'
6
+
5
7
  const zoomSettleMs = 150
6
8
 
9
+ // a wheel event this soon after one the view took belongs to the same gesture,
10
+ // so the view keeps it at an edge rather than handing the momentum to the page
11
+ const wheelLatchMs = 150
12
+
13
+ type Axes = 'x' | 'y' | 'xy'
14
+
15
+ /**
16
+ * Whether a vertical wheel `delta` moves a view at `scrollY`. At the top or
17
+ * bottom, or when every row fits, the page scrolls instead, so an embedded
18
+ * viewer never traps the reader scrolling past it.
19
+ */
20
+ export function canScrollY(model: MsaViewModel, delta: number) {
21
+ const { scrollY, maxScrollY } = model
22
+ return delta > 0 ? scrollY > maxScrollY : delta < 0 && scrollY < 0
23
+ }
24
+
25
+ // Safari reads a touch-action holding pinch-zoom as invalid and falls back to
26
+ // auto, which hands every swipe to the page
27
+ const pinchZoom =
28
+ typeof CSS !== 'undefined' && CSS.supports('touch-action', 'pinch-zoom')
29
+ ? 'pinch-zoom'
30
+ : undefined
31
+
32
+ function withPinch(pan: string) {
33
+ return pinchZoom ? `${pan} ${pinchZoom}` : pan
34
+ }
35
+
36
+ /**
37
+ * The CSS touch-action for a pannable area: the browser keeps a touch pan
38
+ * along every axis the view cannot scroll, and pinch zoom on the page where it
39
+ * supports that value.
40
+ */
41
+ export function panTouchAction(model: MsaViewModel, axes: Axes) {
42
+ const x = axes.includes('x') && model.maxScrollX < 0
43
+ const y = axes.includes('y') && model.maxScrollY < 0
44
+ return x && y
45
+ ? (pinchZoom ?? 'none')
46
+ : x
47
+ ? withPinch('pan-y')
48
+ : y
49
+ ? withPinch('pan-x')
50
+ : 'auto'
51
+ }
52
+
53
+ /**
54
+ * Wheel scrolling, wheel zoom and pointer-drag panning of `model` over `ref`,
55
+ * along `axes`. The returned `onPointerDown` starts a pan with a mouse, a pen
56
+ * or a finger; put `touchAction` on the same element.
57
+ */
7
58
  export function useWheelScroll({
8
59
  ref,
9
- onScrollX,
10
- onScrollY,
11
- onZoom,
12
- onZoomSettle,
60
+ model,
61
+ axes,
62
+ zoom,
13
63
  onWheelScroll,
14
- scrollZoom,
15
64
  }: {
16
65
  ref: React.RefObject<HTMLDivElement | null>
17
- onScrollX?: (delta: number) => void
18
- onScrollY?: (delta: number) => void
19
- onZoom?: (scaleFactor: number, offsetX: number, offsetY: number) => void
20
- onZoomSettle?: () => void
66
+ model: MsaViewModel
67
+ axes: Axes
68
+ zoom?: boolean
21
69
  onWheelScroll?: (event: WheelEvent) => void
22
- scrollZoom?: boolean
23
70
  }) {
24
71
  const scheduled = useRef(false)
25
72
  const zoomFrame = useRef<number | undefined>(undefined)
@@ -28,13 +75,18 @@ export function useWheelScroll({
28
75
  )
29
76
  const deltaX = useRef(0)
30
77
  const deltaY = useRef(0)
78
+ const lastTaken = useRef(Number.NEGATIVE_INFINITY)
31
79
  const zoomSmoother = useRef(new WheelZoomSmoother())
32
80
  const zoomX = useRef(0)
33
81
  const zoomY = useRef(0)
34
82
  const lastZoomTime = useRef<number | null>(null)
35
- const prevX = useRef(0)
36
- const prevY = useRef(0)
37
- const [mouseDragging, setMouseDragging] = useState(false)
83
+ const prev = useRef({ x: 0, y: 0 })
84
+ const latest = useRef({ x: 0, y: 0 })
85
+ const panFrame = useRef(false)
86
+ // the pointer that started the pan; a second finger neither moves nor ends it
87
+ const [panPointer, setPanPointer] = useState<number>()
88
+ const x = axes.includes('x')
89
+ const y = axes.includes('y')
38
90
 
39
91
  useEffect(() => {
40
92
  const curr = ref.current
@@ -47,11 +99,10 @@ export function useWheelScroll({
47
99
  // shift is the escape hatch to pan instead.
48
100
  const isCtrlZoom = event.ctrlKey || event.metaKey
49
101
  const isScrollZoom =
50
- scrollZoom &&
102
+ model.scrollZoom &&
51
103
  !event.shiftKey &&
52
104
  Math.abs(event.deltaY) >= Math.abs(event.deltaX)
53
- if (onZoom && (isCtrlZoom || isScrollZoom)) {
54
- const zoom = onZoom
105
+ if (zoom && (isCtrlZoom || isScrollZoom)) {
55
106
  const rect = curr!.getBoundingClientRect()
56
107
  zoomSmoother.current.add(event.deltaY, event.deltaMode, isCtrlZoom)
57
108
  zoomX.current = event.clientX - rect.left
@@ -62,14 +113,14 @@ export function useWheelScroll({
62
113
  const elapsed =
63
114
  lastZoomTime.current === null ? 16.67 : now - lastZoomTime.current
64
115
  lastZoomTime.current = now
65
- zoom(
116
+ model.wheelZoomToPos(
66
117
  zoomSmoother.current.step(Math.min(100, elapsed)),
67
118
  zoomX.current,
68
119
  zoomY.current,
69
120
  )
70
121
  clearTimeout(zoomSettle.current)
71
122
  zoomSettle.current = setTimeout(() => {
72
- onZoomSettle?.()
123
+ model.endWheelZoom()
73
124
  }, zoomSettleMs)
74
125
  zoomFrame.current = zoomSmoother.current.idle
75
126
  ? undefined
@@ -81,30 +132,30 @@ export function useWheelScroll({
81
132
  event.stopPropagation()
82
133
  return
83
134
  }
84
- // only swallow the wheel event on an axis we actually handle, otherwise a
85
- // vertical wheel over an x-only scroller (e.g. a Track) blocks page scroll
86
- const consumeX = !!onScrollX && event.deltaX !== 0
87
- const consumeY = !!onScrollY && event.deltaY !== 0
88
- if (consumeX || consumeY) {
89
- onWheelScroll?.(event)
90
- if (onScrollX) {
91
- deltaX.current += event.deltaX
135
+ // the dominant axis decides: a trackpad's vertical swipe carries a pixel
136
+ // or two of deltaX, which must not hold the page at the bottom row. A
137
+ // horizontal wheel stays taken at the edges, where the page would read
138
+ // it as a swipe back through history.
139
+ const { deltaX: dx, deltaY: dy } = event
140
+ const vertical = Math.abs(dy) >= Math.abs(dx)
141
+ const latched = event.timeStamp - lastTaken.current < wheelLatchMs
142
+ const takeY =
143
+ y && dy !== 0 && (latched || canScrollY(model, dy) || !vertical)
144
+ const takeX = x && dx !== 0 && (!vertical || takeY)
145
+ if (takeX || takeY) {
146
+ if (takeY && vertical) {
147
+ lastTaken.current = event.timeStamp
92
148
  }
93
- if (onScrollY) {
94
- deltaY.current += event.deltaY
95
- }
96
-
149
+ onWheelScroll?.(event)
150
+ deltaX.current += takeX ? event.deltaX : 0
151
+ deltaY.current += takeY ? event.deltaY : 0
97
152
  if (!scheduled.current) {
98
153
  scheduled.current = true
99
154
  requestAnimationFrame(() => {
100
- if (onScrollX) {
101
- onScrollX(-deltaX.current)
102
- deltaX.current = 0
103
- }
104
- if (onScrollY) {
105
- onScrollY(-deltaY.current)
106
- deltaY.current = 0
107
- }
155
+ model.doScrollX(-deltaX.current)
156
+ model.doScrollY(-deltaY.current)
157
+ deltaX.current = 0
158
+ deltaY.current = 0
108
159
  scheduled.current = false
109
160
  })
110
161
  }
@@ -121,64 +172,53 @@ export function useWheelScroll({
121
172
  }
122
173
  clearTimeout(zoomSettle.current)
123
174
  }
124
- }, [
125
- ref,
126
- onScrollX,
127
- onScrollY,
128
- onZoom,
129
- onZoomSettle,
130
- onWheelScroll,
131
- scrollZoom,
132
- ])
175
+ }, [ref, model, x, y, zoom, onWheelScroll])
133
176
 
134
177
  useEffect(() => {
135
- if (!mouseDragging) {
178
+ if (panPointer === undefined) {
136
179
  return
137
180
  }
138
- function globalMouseMove(event: MouseEvent) {
181
+ // a frame applies the distance from the last applied position to the
182
+ // newest one, so a move landing while a frame is pending is not dropped
183
+ function onMove(event: PointerEvent) {
184
+ if (event.pointerId !== panPointer) {
185
+ return
186
+ }
139
187
  event.preventDefault()
140
- const distanceX = event.clientX - prevX.current
141
- const distanceY = event.clientY - prevY.current
142
- if ((distanceX && onScrollX) || (distanceY && onScrollY)) {
143
- if (!scheduled.current) {
144
- scheduled.current = true
145
- requestAnimationFrame(() => {
146
- onScrollX?.(distanceX)
147
- onScrollY?.(distanceY)
148
- scheduled.current = false
149
- prevX.current = event.clientX
150
- prevY.current = event.clientY
151
- })
152
- }
188
+ latest.current = { x: event.clientX, y: event.clientY }
189
+ if (!panFrame.current) {
190
+ panFrame.current = true
191
+ requestAnimationFrame(() => {
192
+ const to = latest.current
193
+ model.doScrollX(x ? to.x - prev.current.x : 0)
194
+ model.doScrollY(y ? to.y - prev.current.y : 0)
195
+ prev.current = to
196
+ panFrame.current = false
197
+ })
153
198
  }
154
199
  }
155
-
156
- function globalMouseUp() {
157
- prevX.current = 0
158
- prevY.current = 0
159
- setMouseDragging(false)
200
+ function onUp(event: PointerEvent) {
201
+ if (event.pointerId === panPointer) {
202
+ setPanPointer(undefined)
203
+ }
160
204
  }
161
-
162
- window.addEventListener('mousemove', globalMouseMove, true)
163
- window.addEventListener('mouseup', globalMouseUp, true)
205
+ window.addEventListener('pointermove', onMove, true)
206
+ window.addEventListener('pointerup', onUp, true)
207
+ window.addEventListener('pointercancel', onUp, true)
164
208
  return () => {
165
- window.removeEventListener('mousemove', globalMouseMove, true)
166
- window.removeEventListener('mouseup', globalMouseUp, true)
209
+ window.removeEventListener('pointermove', onMove, true)
210
+ window.removeEventListener('pointerup', onUp, true)
211
+ window.removeEventListener('pointercancel', onUp, true)
167
212
  }
168
- }, [mouseDragging, onScrollX, onScrollY])
213
+ }, [panPointer, model, x, y])
169
214
 
170
- function onMouseDown(event: React.MouseEvent) {
171
- if (event.button === 0) {
172
- prevX.current = event.clientX
173
- prevY.current = event.clientY
174
- setMouseDragging(true)
215
+ function onPointerDown(event: React.PointerEvent) {
216
+ if (event.button === 0 && event.isPrimary && panPointer === undefined) {
217
+ prev.current = { x: event.clientX, y: event.clientY }
218
+ latest.current = prev.current
219
+ setPanPointer(event.pointerId)
175
220
  }
176
221
  }
177
222
 
178
- function onMouseUp(event: React.MouseEvent) {
179
- event.preventDefault()
180
- setMouseDragging(false)
181
- }
182
-
183
- return { onMouseDown, onMouseUp }
223
+ return { onPointerDown, touchAction: panTouchAction(model, axes) }
184
224
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '9.1.1'
1
+ export const version = '9.3.0'
@@ -1,5 +1,7 @@
1
1
  import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
2
2
 
3
+ import { letterColors } from './scales.ts'
4
+
3
5
  import type { MSAViewerProps } from './components/MSAViewer.tsx'
4
6
  import type { ColorScheme, Highlight } from './types.ts'
5
7
 
@@ -7,7 +9,7 @@ export function colorSchemeSnapshot(scheme: ColorScheme | undefined) {
7
9
  return typeof scheme === 'string'
8
10
  ? { colorSchemeName: scheme }
9
11
  : scheme
10
- ? { customColorScheme: scheme.map }
12
+ ? { customColorScheme: letterColors(scheme) }
11
13
  : {}
12
14
  }
13
15