@astronox/core 0.5.9 → 0.5.11

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/client/scroll.ts +14 -1
  3. package/src/components/data-display/IconList/IconList.astro +20 -2
  4. package/src/components/data-display/MetricTile/MetricTileContent.astro +6 -1
  5. package/src/components/feedback/Tooltip/Tooltip.css +9 -1
  6. package/src/components/feedback/Tooltip/tooltipController.ts +12 -9
  7. package/src/components/media/EmbedFrame/EmbedFrame.astro +9 -0
  8. package/src/components/media/EmbedFrame/EmbedFrame.css +5 -0
  9. package/src/components/media/EmbedFrame/embedFrameController.ts +84 -1
  10. package/src/display/contracts.ts +1 -0
  11. package/src/patterns/CircuitPuzzle/CircuitPuzzle.astro +3 -0
  12. package/src/patterns/CircuitPuzzle/circuitPuzzleController.ts +49 -0
  13. package/src/patterns/LightMazePuzzle/LightMazePuzzle.astro +1 -0
  14. package/src/patterns/LightMazePuzzle/lightMazePuzzleController.ts +43 -56
  15. package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.astro +1 -0
  16. package/src/patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css +2 -2
  17. package/src/patterns/MagneticFieldPuzzle/magneticFieldPuzzleController.ts +50 -64
  18. package/src/patterns/PuzzleGrid/PuzzleGrid.astro +2 -0
  19. package/src/patterns/PuzzleGrid/keyboard.ts +220 -0
  20. package/src/patterns/PuzzleGrid/movement.css +4 -0
  21. package/src/patterns/PuzzleGrid/movement.ts +67 -3
  22. package/src/patterns/PuzzleGrid/selectionDismissal.ts +2 -2
  23. package/src/patterns/SlidingPuzzle/SlidingPuzzle.astro +1 -0
  24. package/src/patterns/SlidingPuzzle/slidingPuzzleController.ts +71 -74
  25. package/src/patterns/Table/TableCell.astro +1 -1
  26. package/src/patterns/Table/TableCellValue.astro +6 -1
  27. package/src/styles/tokens.css +4 -0
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  animatePieceMovement,
3
3
  cancelDragFrame,
4
+ clearPuzzleMovementPreview,
4
5
  findDirectionalDropDestination,
6
+ getPuzzleMoveTarget,
5
7
  hasLeftOriginCell,
6
8
  renderPathStream,
7
9
  scheduleDragFrame,
@@ -12,6 +14,10 @@ import {
12
14
  setPuzzleGridState,
13
15
  } from '../PuzzleGrid/state';
14
16
  import { initPuzzleSelectionDismissal } from '../PuzzleGrid/selectionDismissal';
17
+ import {
18
+ initPuzzleKeyboard,
19
+ type PuzzleKeyboard,
20
+ } from '../PuzzleGrid/keyboard';
15
21
  import type {
16
22
  MagneticFieldDirection,
17
23
  MagneticFieldPolarity,
@@ -33,6 +39,7 @@ type MagneticFieldInstance = {
33
39
  selectedPieceId: string | null;
34
40
  drag: PuzzleDrag | null;
35
41
  ignoreNextClick: boolean;
42
+ keyboard?: PuzzleKeyboard;
36
43
  };
37
44
 
38
45
  const dragThreshold = 12;
@@ -48,19 +55,14 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
48
55
  });
49
56
  initPuzzleSelectionDismissal(root, {
50
57
  dismiss: () => selectPiece(root, null),
51
- isSelectionTarget: (target) => {
58
+ isSelectionTarget: (target, event) => {
52
59
  const panel = getActivePanel(root);
53
60
  if (!panel || panel.dataset.puzzleStatus === 'valid') return false;
54
61
 
55
62
  const piece = target.closest<HTMLElement>('[data-field-piece]');
56
63
  if (piece && panel.contains(piece)) return true;
57
64
 
58
- const destination = target.closest<HTMLButtonElement>(
59
- '[data-field-destination]',
60
- );
61
- return Boolean(
62
- destination && panel.contains(destination) && !destination.disabled,
63
- );
65
+ return Boolean(getPuzzleMoveTarget(panel, event));
64
66
  },
65
67
  });
66
68
 
@@ -79,14 +81,9 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
79
81
  return;
80
82
  }
81
83
 
82
- const destination = target.closest<HTMLButtonElement>(
83
- '[data-field-destination]',
84
- );
85
- if (destination && !destination.disabled) {
86
- const pieceId = destination.dataset.destinationPiece;
87
- const direction = destination.dataset.destinationDirection as
88
- MagneticFieldDirection | undefined;
89
- if (pieceId && direction) movePiece(root, pieceId, direction);
84
+ const movement = getPuzzleMoveTarget(getActivePanel(root), event);
85
+ if (movement) {
86
+ movePiece(root, movement.pieceId, movement.direction);
90
87
  return;
91
88
  }
92
89
 
@@ -102,12 +99,11 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
102
99
  const pieceId = piece?.dataset.fieldPiece;
103
100
  if (!piece || !pieceId || !instance) return;
104
101
  const panel = getActivePanel(root);
105
- const originCell = panel
106
- ? findCell(panel, {
107
- row: Number(piece.dataset.pieceRow),
108
- column: Number(piece.dataset.pieceColumn),
109
- })
110
- : null;
102
+ if (!panel || panel.dataset.puzzleStatus === 'valid') return;
103
+ const originCell = findCell(panel, {
104
+ row: Number(piece.dataset.pieceRow),
105
+ column: Number(piece.dataset.pieceColumn),
106
+ });
111
107
  if (!originCell) return;
112
108
  const pieceBounds = piece.getBoundingClientRect();
113
109
 
@@ -155,24 +151,20 @@ export function initMagneticFieldPuzzle(root: HTMLElement): void {
155
151
  finishDrag(root, event, false);
156
152
  });
157
153
 
158
- root.addEventListener('keydown', (event) => {
159
- if (event.altKey || event.ctrlKey || event.metaKey) return;
160
- const direction = arrowDirections[event.key.toLowerCase()];
161
- if (!direction) {
162
- if (event.key === 'Escape') selectPiece(root, null);
163
- return;
164
- }
165
-
166
- const piece = (event.target as Element | null)?.closest<HTMLElement>(
167
- '[data-field-piece]',
168
- );
169
- const pieceId =
170
- piece?.dataset.fieldPiece ??
171
- instances.get(root)?.selectedPieceId ??
172
- undefined;
173
- if (!pieceId) return;
174
- event.preventDefault();
175
- movePiece(root, pieceId, direction);
154
+ const instance = instances.get(root)!;
155
+ instance.keyboard = initPuzzleKeyboard(root, {
156
+ getPanel: () => getActivePanel(root),
157
+ getSelected: () =>
158
+ getActivePanel(root)?.querySelector<HTMLButtonElement>(
159
+ '[data-field-piece][data-piece-selected]',
160
+ ) ?? null,
161
+ select: (piece) => {
162
+ if (!piece) cancelDrag(instance);
163
+ selectPiece(root, piece?.dataset.fieldPiece ?? null);
164
+ },
165
+ move: (piece, direction) =>
166
+ movePiece(root, piece.dataset.fieldPiece!, direction),
167
+ isLocked: () => instance.drag !== null,
176
168
  });
177
169
  }
178
170
 
@@ -286,11 +278,11 @@ function updateDestinations(root: HTMLElement): void {
286
278
  for (const movement of getMagneticFieldMoves(puzzle, state, pieceId)) {
287
279
  const directionLabel = readDirectionLabel(labels, movement.direction);
288
280
  const board = panel.querySelector<HTMLElement>('[data-field-board]');
289
- if (board) renderPathStream(board, movement);
281
+ if (board) renderPathStream(board, movement, pieceId);
290
282
 
291
283
  const destination = findCell(panel, movement.to);
292
284
  if (!destination) continue;
293
- destination.removeAttribute('tabindex');
285
+ destination.tabIndex = -1;
294
286
  destination.setAttribute('data-field-destination', '');
295
287
  destination.setAttribute('data-puzzle-cell-interactive', '');
296
288
  destination.dataset.destinationPiece = pieceId;
@@ -350,13 +342,15 @@ function initializeActivePuzzle(root: HTMLElement): void {
350
342
  const initial = createMagneticFieldInitialState(puzzle);
351
343
 
352
344
  instance.selectedPieceId = null;
353
- instance.drag = null;
345
+ cancelDrag(instance);
354
346
  instance.ignoreNextClick = false;
355
347
  panel.dataset.puzzleStatus = 'incomplete';
356
348
  const grid = panel.querySelector<HTMLElement>('[data-puzzle-grid]');
357
349
  if (grid) setPuzzleGridState(grid, null);
358
350
  renderState(panel, puzzle, initial);
351
+ syncPieceSelection(panel, null);
359
352
  hideMovePreviews(panel);
353
+ instance.keyboard?.reset();
360
354
  emitChange(root, puzzle.id, initial);
361
355
  }
362
356
 
@@ -458,14 +452,7 @@ function finishDrag(
458
452
  ? findDirectionalDropDestination(drag, deltaX, deltaY)
459
453
  : null;
460
454
 
461
- cancelDragFrame(drag);
462
- drag.element.removeAttribute('data-piece-dragging');
463
- drag.element.style.removeProperty('--piece-drag-x');
464
- drag.element.style.removeProperty('--piece-drag-y');
465
- if (drag.element.hasPointerCapture(event.pointerId)) {
466
- drag.element.releasePointerCapture(event.pointerId);
467
- }
468
- instance.drag = null;
455
+ cancelDrag(instance);
469
456
 
470
457
  if (commit && drag.moved) {
471
458
  instance.ignoreNextClick = true;
@@ -492,12 +479,20 @@ function getDropDestinations(root: HTMLElement): HTMLButtonElement[] {
492
479
  ];
493
480
  }
494
481
 
482
+ function cancelDrag(instance: MagneticFieldInstance): void {
483
+ const drag = instance.drag;
484
+ if (!drag) return;
485
+ instance.drag = null;
486
+ cancelDragFrame(drag);
487
+ drag.element.removeAttribute('data-piece-dragging');
488
+ drag.element.style.removeProperty('--piece-drag-x');
489
+ drag.element.style.removeProperty('--piece-drag-y');
490
+ if (drag.element.hasPointerCapture(drag.pointerId))
491
+ drag.element.releasePointerCapture(drag.pointerId);
492
+ }
493
+
495
494
  function hideMovePreviews(panel: HTMLElement): void {
496
- for (const stream of panel.querySelectorAll<HTMLElement>(
497
- '[data-puzzle-path-stream]',
498
- )) {
499
- stream.remove();
500
- }
495
+ clearPuzzleMovementPreview(panel);
501
496
 
502
497
  for (const destination of panel.querySelectorAll<HTMLButtonElement>(
503
498
  '[data-field-destination]',
@@ -657,12 +652,3 @@ function formatTemplate(
657
652
  function capitalize(value: string): string {
658
653
  return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
659
654
  }
660
-
661
- const arrowDirections: Readonly<
662
- Partial<Record<string, MagneticFieldDirection>>
663
- > = {
664
- arrowup: 'north',
665
- arrowright: 'east',
666
- arrowdown: 'south',
667
- arrowleft: 'west',
668
- };
@@ -16,6 +16,8 @@ const { class: className, label, rows, columns, ...attrs }: Props = Astro.props;
16
16
  role="group"
17
17
  aria-label={label}
18
18
  data-puzzle-grid
19
+ data-puzzle-rows={rows}
20
+ data-puzzle-columns={columns}
19
21
  style={`--puzzle-columns: ${columns}; --puzzle-rows: ${rows};`}
20
22
  >
21
23
  <slot />
@@ -0,0 +1,220 @@
1
+ import {
2
+ getPuzzleMoveTarget,
3
+ puzzleArrowDirections,
4
+ type PuzzleDirection,
5
+ } from './movement';
6
+
7
+ type PuzzleKeyboardOptions = {
8
+ getPanel: () => HTMLElement | undefined;
9
+ getSelected: () => HTMLButtonElement | null;
10
+ select: (control: HTMLButtonElement | null) => void;
11
+ move: (control: HTMLButtonElement, direction: PuzzleDirection) => void;
12
+ isLocked?: () => boolean;
13
+ };
14
+
15
+ export type PuzzleKeyboard = {
16
+ reset: () => void;
17
+ destroy: () => void;
18
+ };
19
+
20
+ export function initPuzzleKeyboard(
21
+ root: HTMLElement,
22
+ {
23
+ getPanel,
24
+ getSelected,
25
+ select,
26
+ move,
27
+ isLocked = () => false,
28
+ }: PuzzleKeyboardOptions,
29
+ ): PuzzleKeyboard {
30
+ const controller = new AbortController();
31
+ const options = { signal: controller.signal };
32
+ let engaged = false;
33
+ let released = false;
34
+ let pointerFocus = false;
35
+ let focusing = false;
36
+ let entry: HTMLButtonElement | null = null;
37
+ const getBoard = (): HTMLElement | null =>
38
+ getPanel()?.querySelector<HTMLElement>('[data-puzzle-grid]') ?? null;
39
+ const isComplete = (): boolean =>
40
+ getPanel()?.dataset.puzzleStatus === 'valid';
41
+ const getControls = (): HTMLButtonElement[] => [
42
+ ...(getBoard()?.querySelectorAll<HTMLButtonElement>(
43
+ '[data-puzzle-control]:not(:disabled)',
44
+ ) ?? []),
45
+ ];
46
+
47
+ const sync = (): void => {
48
+ const controls = getControls();
49
+ const selected = getSelected();
50
+ entry =
51
+ (selected && controls.includes(selected) ? selected : null) ??
52
+ (entry && controls.includes(entry) ? entry : null) ??
53
+ controls[0] ??
54
+ null;
55
+ if (!selected || isComplete()) engaged = false;
56
+ const board = getBoard();
57
+ if (board) board.tabIndex = released && !isComplete() ? 0 : -1;
58
+ for (const control of controls) {
59
+ control.tabIndex =
60
+ !released && !isComplete() && control === entry ? 0 : -1;
61
+ control.setAttribute(
62
+ 'aria-keyshortcuts',
63
+ 'ArrowUp ArrowRight ArrowDown ArrowLeft Tab Shift+Tab Escape Enter Space',
64
+ );
65
+ }
66
+ };
67
+
68
+ const focus = (control: HTMLElement): void => {
69
+ focusing = true;
70
+ control.focus({ preventScroll: true });
71
+ focusing = false;
72
+ };
73
+
74
+ const activate = (control: HTMLButtonElement): void => {
75
+ released = false;
76
+ entry = control;
77
+ if (getSelected() !== control) select(control);
78
+ sync();
79
+ engaged = true;
80
+ focus(control);
81
+ };
82
+
83
+ root.addEventListener(
84
+ 'pointerdown',
85
+ () => {
86
+ pointerFocus = true;
87
+ },
88
+ { ...options, capture: true },
89
+ );
90
+ root.addEventListener(
91
+ 'pointercancel',
92
+ () => {
93
+ pointerFocus = false;
94
+ },
95
+ options,
96
+ );
97
+ root.addEventListener(
98
+ 'focusin',
99
+ (event) => {
100
+ if (focusing || pointerFocus || isLocked() || isComplete()) return;
101
+ if (!(event.target instanceof HTMLButtonElement)) return;
102
+ if (getControls().includes(event.target)) activate(event.target);
103
+ },
104
+ options,
105
+ );
106
+ root.addEventListener(
107
+ 'focusout',
108
+ (event) => {
109
+ if (
110
+ event.relatedTarget instanceof Node &&
111
+ getBoard()?.contains(event.relatedTarget)
112
+ )
113
+ return;
114
+ engaged = false;
115
+ released = false;
116
+ pointerFocus = false;
117
+ select(null);
118
+ sync();
119
+ },
120
+ options,
121
+ );
122
+ root.addEventListener(
123
+ 'click',
124
+ (event) => {
125
+ if (!(event.target instanceof Element) || isLocked()) return;
126
+ const selected = getSelected();
127
+ if (selected && getPuzzleMoveTarget(getPanel(), event)) focus(selected);
128
+ },
129
+ { ...options, capture: true },
130
+ );
131
+ root.addEventListener(
132
+ 'click',
133
+ (event) => {
134
+ pointerFocus = false;
135
+ if (
136
+ !(event.target instanceof Node) ||
137
+ !getBoard()?.contains(event.target)
138
+ )
139
+ return;
140
+ const selected = getSelected();
141
+ if (selected && !isComplete()) {
142
+ released = false;
143
+ engaged = true;
144
+ sync();
145
+ focus(selected);
146
+ } else if (getBoard()?.contains(document.activeElement)) {
147
+ released = true;
148
+ sync();
149
+ const board = getBoard();
150
+ if (board) focus(board);
151
+ } else sync();
152
+ },
153
+ options,
154
+ );
155
+ root.addEventListener(
156
+ 'keydown',
157
+ (event) => {
158
+ pointerFocus = false;
159
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
160
+ const board = getBoard();
161
+ if (!(event.target instanceof Node) || !board?.contains(event.target))
162
+ return;
163
+ if (event.key === 'Escape') {
164
+ event.preventDefault();
165
+ event.stopPropagation();
166
+ engaged = false;
167
+ released = true;
168
+ select(null);
169
+ sync();
170
+ focus(board);
171
+ return;
172
+ }
173
+ if (isComplete()) return;
174
+ const direction = puzzleArrowDirections[event.key];
175
+ if (event.key === 'Tab') {
176
+ if (!engaged) return;
177
+ event.preventDefault();
178
+ if (isLocked()) return;
179
+ const controls = getControls();
180
+ if (!controls.length) return;
181
+ const index = controls.indexOf(getSelected() ?? entry ?? controls[0]);
182
+ const next =
183
+ (index + (event.shiftKey ? -1 : 1) + controls.length) %
184
+ controls.length;
185
+ activate(controls[next]);
186
+ } else if (isLocked()) {
187
+ if (direction || event.key === 'Enter' || event.key === ' ')
188
+ event.preventDefault();
189
+ } else if (direction) {
190
+ event.preventDefault();
191
+ const selected = getSelected() ?? entry ?? getControls()[0];
192
+ if (!selected) return;
193
+ activate(selected);
194
+ move(selected, direction);
195
+ sync();
196
+ } else if (
197
+ event.target === board &&
198
+ (event.key === 'Enter' || event.key === ' ')
199
+ ) {
200
+ event.preventDefault();
201
+ const selected = entry ?? getControls()[0];
202
+ if (selected) activate(selected);
203
+ }
204
+ },
205
+ options,
206
+ );
207
+ root.addEventListener('puzzle:change', sync, options);
208
+ root.addEventListener('puzzle:complete', sync, options);
209
+ sync();
210
+ return {
211
+ reset: () => {
212
+ engaged = false;
213
+ released = false;
214
+ entry = null;
215
+ pointerFocus = false;
216
+ sync();
217
+ },
218
+ destroy: () => controller.abort(),
219
+ };
220
+ }
@@ -1,3 +1,7 @@
1
+ .puzzle-grid__cell[data-puzzle-move-direction]:not(:disabled) {
2
+ cursor: pointer;
3
+ }
4
+
1
5
  .puzzle-grid__path-stream {
2
6
  --puzzle-path-rotation: 0deg;
3
7
 
@@ -14,7 +14,60 @@ export type PuzzleDrag = {
14
14
  moved: boolean;
15
15
  };
16
16
 
17
- type Direction = 'north' | 'east' | 'south' | 'west';
17
+ export type PuzzleDirection = 'north' | 'east' | 'south' | 'west';
18
+
19
+ export const puzzleArrowDirections: Readonly<
20
+ Partial<Record<string, PuzzleDirection>>
21
+ > = {
22
+ ArrowUp: 'north',
23
+ ArrowRight: 'east',
24
+ ArrowDown: 'south',
25
+ ArrowLeft: 'west',
26
+ };
27
+
28
+ export function getPuzzleMoveTarget(
29
+ panel: HTMLElement | undefined,
30
+ event: MouseEvent,
31
+ ): { pieceId: string; direction: PuzzleDirection } | null {
32
+ const target = event.target;
33
+ if (!(target instanceof Element) || !panel?.contains(target)) return null;
34
+ let cell = target.closest<HTMLButtonElement>('[data-puzzle-move-direction]');
35
+ if (
36
+ !cell &&
37
+ target instanceof HTMLElement &&
38
+ target.matches('[data-puzzle-grid]')
39
+ ) {
40
+ const bounds = target.getBoundingClientRect();
41
+ const x = event.clientX - bounds.left - target.clientLeft;
42
+ const y = event.clientY - bounds.top - target.clientTop;
43
+ if (x < 0 || y < 0 || x >= target.clientWidth || y >= target.clientHeight)
44
+ return null;
45
+ const column = Math.floor(
46
+ (x * Number(target.dataset.puzzleColumns)) / target.clientWidth,
47
+ );
48
+ const row = Math.floor(
49
+ (y * Number(target.dataset.puzzleRows)) / target.clientHeight,
50
+ );
51
+ cell = target.querySelector<HTMLButtonElement>(
52
+ `[data-puzzle-move-direction][data-cell-row="${row}"][data-cell-column="${column}"]`,
53
+ );
54
+ }
55
+ if (!cell || cell.disabled) return null;
56
+ const pieceId = cell.dataset.puzzleMovePiece;
57
+ const direction = cell.dataset.puzzleMoveDirection as PuzzleDirection;
58
+ return pieceId ? { pieceId, direction } : null;
59
+ }
60
+
61
+ export function clearPuzzleMovementPreview(panel: HTMLElement): void {
62
+ for (const stream of panel.querySelectorAll('[data-puzzle-path-stream]'))
63
+ stream.remove();
64
+ for (const cell of panel.querySelectorAll<HTMLElement>(
65
+ '[data-puzzle-move-direction]',
66
+ )) {
67
+ delete cell.dataset.puzzleMoveDirection;
68
+ delete cell.dataset.puzzleMovePiece;
69
+ }
70
+ }
18
71
 
19
72
  const pathChevronsPerCell = 3;
20
73
  const pathDurationPerCellMs = 2100;
@@ -78,7 +131,7 @@ export function findDirectionalDropDestination(
78
131
  deltaX: number,
79
132
  deltaY: number,
80
133
  ): HTMLButtonElement | null {
81
- let direction: Direction;
134
+ let direction: PuzzleDirection;
82
135
  if (Math.abs(deltaX) >= Math.abs(deltaY)) {
83
136
  direction = deltaX >= 0 ? 'east' : 'west';
84
137
  } else {
@@ -99,12 +152,23 @@ export function renderPathStream(
99
152
  movement: {
100
153
  from: { row: number; column: number };
101
154
  to: { row: number; column: number };
102
- direction: Direction;
155
+ direction: PuzzleDirection;
103
156
  },
157
+ pieceId: string,
104
158
  ): void {
105
159
  const rowDistance = movement.to.row - movement.from.row;
106
160
  const columnDistance = movement.to.column - movement.from.column;
107
161
  const cellDistance = Math.abs(rowDistance) + Math.abs(columnDistance);
162
+ for (let step = 1; step <= cellDistance; step += 1) {
163
+ const row = movement.from.row + Math.sign(rowDistance) * step;
164
+ const column = movement.from.column + Math.sign(columnDistance) * step;
165
+ const cell = board.querySelector<HTMLButtonElement>(
166
+ `[data-puzzle-cell][data-cell-row="${row}"][data-cell-column="${column}"]`,
167
+ );
168
+ if (!cell || cell.disabled) continue;
169
+ cell.dataset.puzzleMoveDirection = movement.direction;
170
+ cell.dataset.puzzleMovePiece = pieceId;
171
+ }
108
172
  const chevronCount = pathChevronsPerCell * cellDistance;
109
173
  const stream = document.createElement('span');
110
174
  stream.className = 'puzzle-grid__path-stream';
@@ -1,6 +1,6 @@
1
1
  type PuzzleSelectionDismissalOptions = {
2
2
  dismiss: () => void;
3
- isSelectionTarget: (target: Element) => boolean;
3
+ isSelectionTarget: (target: Element, event: PointerEvent) => boolean;
4
4
  };
5
5
 
6
6
  export function initPuzzleSelectionDismissal(
@@ -15,7 +15,7 @@ export function initPuzzleSelectionDismissal(
15
15
  const target = event.target;
16
16
  if (!(target instanceof Node)) return;
17
17
  const element = target instanceof Element ? target : target.parentElement;
18
- if (element && isSelectionTarget(element)) return;
18
+ if (element && isSelectionTarget(element, event)) return;
19
19
 
20
20
  dismiss();
21
21
  const focused = document.activeElement;
@@ -170,6 +170,7 @@ const {
170
170
  type="button"
171
171
  aria-label={labels.piece}
172
172
  aria-pressed="false"
173
+ data-puzzle-control
173
174
  data-sliding-piece
174
175
  style={`--piece-row: ${puzzle.start.row + 1}; --piece-column: ${puzzle.start.column + 1};`}
175
176
  >