@pluot/react 0.1.2 → 0.1.5

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 (55) hide show
  1. package/dist/index.js +1029 -934
  2. package/dist-tsc/Pluot.d.ts.map +1 -1
  3. package/dist-tsc/Pluot.js +36 -27
  4. package/package.json +3 -3
  5. package/src/Pluot.jsx +54 -34
  6. package/dist/C059-BdIta.ttf-Bdc938Uh.js +0 -4
  7. package/dist/C059-Bold.ttf-D54yVEIG.js +0 -4
  8. package/dist/C059-Italic.ttf-DLWXDpX2.js +0 -4
  9. package/dist/C059-Roman.ttf-B_4E2VG-.js +0 -4
  10. package/dist/D050000L.ttf-DThw2vhA.js +0 -4
  11. package/dist/NimbusMonoPS-Bold.ttf-DaeY0qBr.js +0 -4
  12. package/dist/NimbusMonoPS-BoldItalic.ttf-BWf8L4h2.js +0 -4
  13. package/dist/NimbusMonoPS-Italic.ttf-B6sVyRe1.js +0 -4
  14. package/dist/NimbusMonoPS-Regular.ttf-4U95hi4W.js +0 -4
  15. package/dist/NimbusRoman-Bold.ttf-CYfJBZ-D.js +0 -4
  16. package/dist/NimbusRoman-BoldItalic.ttf-IKgFHn3u.js +0 -4
  17. package/dist/NimbusRoman-Italic.ttf-DphHuaaV.js +0 -4
  18. package/dist/NimbusRoman-Regular.ttf-CyDfJ_2L.js +0 -4
  19. package/dist/NimbusSans-Bold.ttf-BgToHb0k.js +0 -4
  20. package/dist/NimbusSans-BoldItalic.ttf-DBTyNZML.js +0 -4
  21. package/dist/NimbusSans-BoldOblique.ttf-BsmbX-RQ.js +0 -4
  22. package/dist/NimbusSans-Italic.ttf-DXn1aKmU.js +0 -4
  23. package/dist/NimbusSans-Oblique.ttf-BnOkISci.js +0 -4
  24. package/dist/NimbusSans-Regular.ttf-BPUWfSNR.js +0 -4
  25. package/dist/NimbusSansNarrow-BdOblique.ttf-pMMXNfDW.js +0 -4
  26. package/dist/NimbusSansNarrow-Bold.ttf-CTqrnZ6z.js +0 -4
  27. package/dist/NimbusSansNarrow-BoldOblique.ttf-CDZKzRWR.js +0 -4
  28. package/dist/NimbusSansNarrow-Oblique.ttf-CmqouKBN.js +0 -4
  29. package/dist/NimbusSansNarrow-Regular.ttf-DOZi8TRM.js +0 -4
  30. package/dist/P052-Bold.ttf-F7VVlUHU.js +0 -4
  31. package/dist/P052-BoldItalic.ttf-CTaOaPJC.js +0 -4
  32. package/dist/P052-Italic.ttf-CfqenRHn.js +0 -4
  33. package/dist/P052-Roman.ttf-B-tZ3T0p.js +0 -4
  34. package/dist/StandardSymbolsPS.ttf-DQ9k8vPT.js +0 -4
  35. package/dist/URWBookman-Demi.ttf-gvfD6bGd.js +0 -4
  36. package/dist/URWBookman-DemiItalic.ttf-CTYueWh5.js +0 -4
  37. package/dist/URWBookman-Light.ttf-DK879rFB.js +0 -4
  38. package/dist/URWBookman-LightItalic.ttf-BWVZJTZv.js +0 -4
  39. package/dist/URWGothic-Book.ttf-BjKm3X4s.js +0 -4
  40. package/dist/URWGothic-BookOblique.ttf-UGkGTKkJ.js +0 -4
  41. package/dist/URWGothic-Demi.ttf-CUhEK02w.js +0 -4
  42. package/dist/URWGothic-DemiOblique.ttf-CP_D_bVd.js +0 -4
  43. package/dist/Z003-MediumItalic.ttf-BXc6HvHA.js +0 -4
  44. package/dist-tsc/3d-view-controls.d.ts +0 -17
  45. package/dist-tsc/3d-view-controls.d.ts.map +0 -1
  46. package/dist-tsc/3d-view-controls.js +0 -223
  47. package/dist-tsc/dom-2d-camera.d.ts +0 -26
  48. package/dist-tsc/dom-2d-camera.d.ts.map +0 -1
  49. package/dist-tsc/dom-2d-camera.js +0 -303
  50. package/dist-tsc/feature-detection.d.ts +0 -5
  51. package/dist-tsc/feature-detection.d.ts.map +0 -1
  52. package/dist-tsc/feature-detection.js +0 -6
  53. package/dist-tsc/lru-store.d.ts +0 -4
  54. package/dist-tsc/lru-store.d.ts.map +0 -1
  55. package/dist-tsc/lru-store.js +0 -137
@@ -1 +1 @@
1
- {"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"AAiDA,uCAgZC"}
1
+ {"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"AA+CA,uCAsaC"}
package/dist-tsc/Pluot.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
3
- import { mat4, vec4 } from "gl-matrix";
2
+ import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback, useId } from "react";
4
3
  import lzs from "lz-string";
5
4
  import { isEqual, throttle } from "lodash-es";
6
- import { FetchStore } from 'zarrita';
7
- import { initialize, getIsWasmReady, render_wasm, pick_wasm, setStore, getStore, getBounds, getCameraMatrixFromBounds, checkWebGpuFeatureDetection, onMouseMove2d, onWheel2d, onMouseMove3d, onWheel3d, } from '@pluot/core';
5
+ import { initialize, getIsWasmReady, render_wasm, pick_wasm, normalizeStores, getStore, getBounds, getCameraMatrixFromBounds, checkWebGpuFeatureDetection, onMouseMove2d, onWheel2d, onMouseMove3d, onWheel3d, } from '@pluot/core';
8
6
  // Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
9
7
  // The requested module 'lz-string' is a CommonJS module,
10
8
  // which may not support all module.exports as named exports."
@@ -34,7 +32,7 @@ function normalizePickingResult(data) {
34
32
  return result;
35
33
  }
36
34
  export function Pluot(props) {
37
- const { width: widthProp, height: heightProp, plotId, plotType, store, storeName: storeNameProp, plotParams, viewMode = "2d", marginBottom = 100.0, marginLeft = 100.0, marginTop = 100.0, marginRight = 100.0, aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
35
+ const { width: widthProp, height: heightProp, plotId, plotType, store: storeProp, storeName: storeNameProp, stores: storesProp, plotParams, viewMode = "2d", marginBottom = 100.0, marginLeft = 100.0, marginTop = 100.0, marginRight = 100.0, aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
38
36
  aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
39
37
  format = "Raster", // "Raster", "Vector"
40
38
  minTimeout = 32, maxTimeout = 5000, allowSimultaneousRenders = true, debugMargins = false, cameraMatrix: controlledCameraMatrix = null, setCameraMatrix: setControlledCameraMatrix = null, enablePicking = true, backgroundColor = undefined, } = props;
@@ -65,19 +63,15 @@ export function Pluot(props) {
65
63
  const width = Math.floor(widthProp);
66
64
  const height = Math.floor(heightProp);
67
65
  const isVector = format === "Vector";
68
- const storeName = useMemo(() => {
69
- if (storeNameProp) {
70
- return storeNameProp;
71
- }
72
- // If store is a string, assume it is a URL and initialize a FetchStore here.
73
- if (store) {
74
- if (typeof store === 'string') {
75
- return setStore(new FetchStore(store), plotId);
76
- }
77
- return setStore(store, plotId);
78
- }
79
- throw new Error("Either storeName or store must be provided.");
80
- }, [storeNameProp, store]);
66
+ // Build the top-level `stores` map that RenderParams expects: a mapping from
67
+ // store name to its derived `ZarrStoreInfo` metadata.
68
+ const stores = useMemo(() => normalizeStores({
69
+ stores: storesProp,
70
+ store: storeProp,
71
+ storeName: storeNameProp,
72
+ plotId,
73
+ register: true,
74
+ }), [storeNameProp, storeProp, storesProp, plotId]);
81
75
  const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
82
76
  const svgRef = useRef(null);
83
77
  const canvasRef = useRef(null);
@@ -95,6 +89,7 @@ export function Pluot(props) {
95
89
  const [didFirstRender, setDidFirstRender] = useState(false);
96
90
  const [bailedEarly, setBailedEarly] = useState(true);
97
91
  const [pickingResult, setPickingResult] = useState(null);
92
+ const progressBarId = useId();
98
93
  useLayoutEffect(() => {
99
94
  initialize().then(() => setIsWasmReady(getIsWasmReady()));
100
95
  }, []);
@@ -149,7 +144,7 @@ export function Pluot(props) {
149
144
  camera_view: cameraMatrix,
150
145
  plot_id: plotId,
151
146
  plot_type: plotType,
152
- store_name: storeName,
147
+ stores,
153
148
  plot_params: plotParams,
154
149
  // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
155
150
  timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
@@ -159,6 +154,7 @@ export function Pluot(props) {
159
154
  svg_include_document: false,
160
155
  };
161
156
  const layerHeight = height - marginTop - marginBottom;
157
+ // TODO: wrap pick_wasm in a try/catch
162
158
  setPickingResult(normalizePickingResult(await pick_wasm(renderParams,
163
159
  // The coordinates are relative to the "layer" (the camera region), not the full width/height.
164
160
  // We also need to flip the Y coordinate so that positive is up.
@@ -209,7 +205,7 @@ export function Pluot(props) {
209
205
  camera_view: cameraMatrix,
210
206
  plot_id: plotId,
211
207
  plot_type: plotType,
212
- store_name: storeName,
208
+ stores,
213
209
  plot_params: plotParams,
214
210
  // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
215
211
  timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
@@ -269,10 +265,12 @@ export function Pluot(props) {
269
265
  currentTimeout.current = minTimeout;
270
266
  setBailedEarly(false); // Update this to hide the loading indicator.
271
267
  // Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
272
- const storeUsed = getStore(renderParams.store_name);
273
- if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
274
- storeUsed.clearCache();
275
- }
268
+ Object.keys(stores).forEach(storeName => {
269
+ const storeUsed = getStore(storeName);
270
+ if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
271
+ storeUsed.clearCache();
272
+ }
273
+ });
276
274
  }
277
275
  }
278
276
  setDidFirstRender(true);
@@ -290,7 +288,7 @@ export function Pluot(props) {
290
288
  // sequence of bailed-early renders starts from the minimum again.
291
289
  useEffect(() => {
292
290
  currentTimeout.current = minTimeout;
293
- }, [plotId, plotType, plotParams, storeName, format,
291
+ }, [plotId, plotType, plotParams, stores, format,
294
292
  width, height, aspectRatioMode, aspectRatioAlignmentMode,
295
293
  marginLeft, marginRight, marginTop, marginBottom,
296
294
  cameraMatrix,
@@ -310,7 +308,7 @@ export function Pluot(props) {
310
308
  }
311
309
  // Render on the next animation frame.
312
310
  throttledRender();
313
- }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
311
+ }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, stores, format,
314
312
  width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
315
313
  return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { width, height, position: "relative", backgroundColor }, children: [!supportsWebGpu ? (_jsx("p", { children: supportsWebGpuMessage })) : null, _jsx("div", { ref: cameraElementRef, style: {
316
314
  position: "absolute",
@@ -319,5 +317,16 @@ export function Pluot(props) {
319
317
  width: width - marginLeft - marginRight,
320
318
  height: height - marginTop - marginBottom,
321
319
  border: `${debugMargins ? 1 : 0}px solid red`,
322
- } }), isVector ? (_jsx("svg", { ref: svgRef, style: { width, height, border: "1px solid black" }, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg" })) : (_jsx("canvas", { ref: canvasRef, style: { width, height, border: "1px solid black" }, width: width, height: height }))] }), bailedEarly ? (_jsx("p", { children: "Loading..." })) : null, _jsx("button", { ref: tempButtonRef, style: { display: 'none' }, children: "Try lookAt" }), pickingResult ? (_jsx("pre", { children: JSON.stringify(pickingResult, null, 2) })) : null] }));
320
+ } }), bailedEarly ? (_jsx("progress", { id: progressBarId, "aria-label": "Loading...", style: {
321
+ bottom: 0,
322
+ left: 0,
323
+ width: '100%',
324
+ position: 'absolute'
325
+ } })) : null, isVector ? (_jsx("svg", { ref: svgRef, style: { width, height, border: `${debugMargins ? 1 : 0}px solid black` }, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", ...(bailedEarly ? ({
326
+ ['aria-busy']: true,
327
+ ['aria-describedby']: progressBarId,
328
+ }) : {}) })) : (_jsx("canvas", { ref: canvasRef, style: { width, height, border: `${debugMargins ? 1 : 0}px solid black` }, width: width, height: height, ...(bailedEarly ? ({
329
+ ['aria-busy']: true,
330
+ ['aria-describedby']: progressBarId,
331
+ }) : {}) }))] }), _jsx("button", { ref: tempButtonRef, style: { display: 'none' }, children: "Try lookAt" }), pickingResult ? (_jsx("pre", { children: JSON.stringify(pickingResult, null, 2) })) : null] }));
323
332
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pluot/react",
3
3
  "private": false,
4
- "version": "0.1.2",
4
+ "version": "0.1.5",
5
5
  "description": "React component for visualization with Pluot",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Mark Keller",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "lightua": "0.1.0",
22
- "zarrita": "^0.5.3",
22
+ "zarrita": "^0.7.3",
23
23
  "quick-lru": "^7.0.1",
24
24
  "d3-zoom": "^3.0.0",
25
25
  "d3-selection": "^3.0.0",
@@ -36,7 +36,7 @@
36
36
  "mouse-wheel": "^1.0.2",
37
37
  "lz-string": "^1.5.0",
38
38
  "lodash-es": "^4.17.23",
39
- "@pluot/core": "0.1.2"
39
+ "@pluot/core": "0.1.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "vitest": "^3.2.4",
package/src/Pluot.jsx CHANGED
@@ -1,12 +1,10 @@
1
- import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
2
- import { mat4, vec4 } from "gl-matrix";
1
+ import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback, useId } from "react";
3
2
  import lzs from "lz-string";
4
3
  import { isEqual, throttle } from "lodash-es";
5
- import { FetchStore } from 'zarrita';
6
4
  import {
7
5
  initialize, getIsWasmReady,
8
6
  render_wasm, pick_wasm,
9
- setStore, getStore,
7
+ normalizeStores, getStore,
10
8
  getBounds, getCameraMatrixFromBounds,
11
9
  checkWebGpuFeatureDetection,
12
10
  onMouseMove2d, onWheel2d,
@@ -53,8 +51,9 @@ export function Pluot(props) {
53
51
  height: heightProp,
54
52
  plotId,
55
53
  plotType,
56
- store,
54
+ store: storeProp,
57
55
  storeName: storeNameProp,
56
+ stores: storesProp,
58
57
  plotParams,
59
58
  viewMode = "2d",
60
59
  marginBottom = 100.0,
@@ -107,19 +106,15 @@ export function Pluot(props) {
107
106
 
108
107
  const isVector = format === "Vector";
109
108
 
110
- const storeName = useMemo(() => {
111
- if (storeNameProp) {
112
- return storeNameProp;
113
- }
114
- // If store is a string, assume it is a URL and initialize a FetchStore here.
115
- if (store) {
116
- if (typeof store === 'string') {
117
- return setStore(new FetchStore(store), plotId);
118
- }
119
- return setStore(store, plotId);
120
- }
121
- throw new Error("Either storeName or store must be provided.");
122
- }, [storeNameProp, store]);
109
+ // Build the top-level `stores` map that RenderParams expects: a mapping from
110
+ // store name to its derived `ZarrStoreInfo` metadata.
111
+ const stores = useMemo(() => normalizeStores({
112
+ stores: storesProp,
113
+ store: storeProp,
114
+ storeName: storeNameProp,
115
+ plotId,
116
+ register: true,
117
+ }), [storeNameProp, storeProp, storesProp, plotId]);
123
118
 
124
119
  const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
125
120
 
@@ -145,12 +140,12 @@ export function Pluot(props) {
145
140
 
146
141
  const [pickingResult, setPickingResult] = useState(null);
147
142
 
143
+ const progressBarId = useId();
148
144
 
149
145
  useLayoutEffect(() => {
150
146
  initialize().then(() => setIsWasmReady(getIsWasmReady()));
151
147
  }, []);
152
148
 
153
-
154
149
  const wheelHandler = useEffectEvent((event) => {
155
150
  const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
156
151
  const nextCameraMatrix = onWheel({
@@ -204,7 +199,7 @@ export function Pluot(props) {
204
199
  camera_view: cameraMatrix,
205
200
  plot_id: plotId,
206
201
  plot_type: plotType,
207
- store_name: storeName,
202
+ stores,
208
203
  plot_params: plotParams,
209
204
  // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
210
205
  timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
@@ -216,6 +211,8 @@ export function Pluot(props) {
216
211
 
217
212
  const layerHeight = height - marginTop - marginBottom;
218
213
 
214
+ // TODO: wrap pick_wasm in a try/catch
215
+
219
216
  setPickingResult(normalizePickingResult(await pick_wasm(
220
217
  renderParams,
221
218
  // The coordinates are relative to the "layer" (the camera region), not the full width/height.
@@ -253,8 +250,6 @@ export function Pluot(props) {
253
250
  }, [viewMode, enablePicking]);
254
251
 
255
252
 
256
-
257
-
258
253
  // The renderFrame callback.
259
254
  // We use useEffectEvent because we want to "see"
260
255
  // the latest values of viewMatrix, plotProps, etc.
@@ -279,7 +274,7 @@ export function Pluot(props) {
279
274
  camera_view: cameraMatrix,
280
275
  plot_id: plotId,
281
276
  plot_type: plotType,
282
- store_name: storeName,
277
+ stores,
283
278
  plot_params: plotParams,
284
279
  // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
285
280
  timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
@@ -348,15 +343,21 @@ export function Pluot(props) {
348
343
  setBailedEarly(false); // Update this to hide the loading indicator.
349
344
 
350
345
  // Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
351
- const storeUsed = getStore(renderParams.store_name);
352
- if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
353
- storeUsed.clearCache();
354
- }
346
+ Object.keys(stores).forEach(storeName => {
347
+ const storeUsed = getStore(storeName);
348
+ if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
349
+ storeUsed.clearCache();
350
+ }
351
+ });
352
+
355
353
  }
356
354
  }
357
355
  setDidFirstRender(true);
358
356
  });
359
357
 
358
+
359
+
360
+
360
361
  const throttledRender = useMemo(
361
362
  () => throttle(
362
363
  renderFrame,
@@ -376,7 +377,7 @@ export function Pluot(props) {
376
377
  // sequence of bailed-early renders starts from the minimum again.
377
378
  useEffect(() => {
378
379
  currentTimeout.current = minTimeout;
379
- }, [plotId, plotType, plotParams, storeName, format,
380
+ }, [plotId, plotType, plotParams, stores, format,
380
381
  width, height, aspectRatioMode, aspectRatioAlignmentMode,
381
382
  marginLeft, marginRight, marginTop, marginBottom,
382
383
  cameraMatrix,
@@ -399,7 +400,7 @@ export function Pluot(props) {
399
400
 
400
401
  // Render on the next animation frame.
401
402
  throttledRender();
402
- }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
403
+ }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, stores, format,
403
404
  width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
404
405
 
405
406
  return (
@@ -419,28 +420,47 @@ export function Pluot(props) {
419
420
  border: `${debugMargins ? 1 : 0}px solid red`,
420
421
  }}
421
422
  />
423
+ {bailedEarly ? (
424
+ <progress
425
+ id={progressBarId}
426
+ aria-label="Loading..."
427
+ style={{
428
+ bottom: 0,
429
+ left: 0,
430
+ width: '100%',
431
+ position: 'absolute'
432
+ }}
433
+ />
434
+ ) : null}
422
435
  {isVector ? (
423
436
  <svg
424
437
  ref={svgRef}
425
- style={{ width, height, border: "1px solid black" }}
438
+ style={{ width, height, border: `${debugMargins ? 1 : 0}px solid black` }}
426
439
  width={width}
427
440
  height={height}
428
441
  viewBox={`0 0 ${width} ${height}`}
429
442
  xmlns="http://www.w3.org/2000/svg"
443
+ {...(bailedEarly ? ({
444
+ ['aria-busy']: true,
445
+ ['aria-describedby']: progressBarId,
446
+ }) : {})}
430
447
  >
431
448
  </svg>
432
449
  ) : (
433
450
  <canvas
434
451
  ref={canvasRef}
435
- style={{ width, height, border: "1px solid black" }}
452
+ style={{ width, height, border: `${debugMargins ? 1 : 0}px solid black` }}
436
453
  width={width}
437
454
  height={height}
455
+ {...(bailedEarly ? ({
456
+ ['aria-busy']: true,
457
+ ['aria-describedby']: progressBarId,
458
+ }) : {})}
459
+
438
460
  />
439
461
  )}
462
+
440
463
  </div>
441
- {bailedEarly ? (
442
- <p>Loading...</p>
443
- ) : null}
444
464
  <button ref={tempButtonRef} style={{ display: 'none' }}>Try lookAt</button>
445
465
  {pickingResult ? (
446
466
  <pre>{JSON.stringify(pickingResult, null, 2)}</pre>