cm-chessboard 8.11.0 → 8.11.2

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/README.md CHANGED
@@ -373,6 +373,9 @@ const chessboard = new Chessboard(document.getElementById("board"), {
373
373
  })
374
374
  ```
375
375
 
376
+ #### Keyboard Shortcut
377
+
378
+ When `movePieceForm` is enabled, press **Shift+Option+E** (Mac) or **Shift+Alt+E** (Windows/Linux) to focus the "Move from" input field.
376
379
 
377
380
  ## Usage with JS Frameworks
378
381
 
@@ -12,6 +12,12 @@ svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button {
12
12
  svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button:hover {
13
13
  fill: rgba(0, 0, 0, 0.2); }
14
14
 
15
+ svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button-group {
16
+ outline: none; }
17
+ svg.cm-chessboard .promotion-dialog-group .promotion-dialog-button-group:focus .promotion-dialog-button {
18
+ stroke: rgba(0, 0, 0, 0.3);
19
+ stroke-width: 2px; }
20
+
15
21
  svg.cm-chessboard .promotion-dialog-group .piece {
16
22
  pointer-events: none; }
17
23
 
@@ -1,4 +1,4 @@
1
- <!doctype html>
1
+ Could <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-chessboard",
3
- "version": "8.11.0",
3
+ "version": "8.11.2",
4
4
  "description": "A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.",
5
5
  "keywords": [
6
6
  "chess",
@@ -142,6 +142,16 @@ class MovePieceForm {
142
142
  extension.registerExtensionPoint(EXTENSION_POINT.moveInputToggled, () => {
143
143
  this.redraw()
144
144
  })
145
+ this.keydownListener = (event) => {
146
+ if (event.shiftKey && event.altKey && event.code === 'KeyE') {
147
+ event.preventDefault()
148
+ this.inputFrom.focus()
149
+ }
150
+ }
151
+ document.addEventListener("keydown", this.keydownListener)
152
+ extension.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
153
+ document.removeEventListener("keydown", this.keydownListener)
154
+ })
145
155
  }
146
156
 
147
157
  redraw() {
@@ -14,6 +14,21 @@ const DISPLAY_STATE = {
14
14
  shown: "shown"
15
15
  }
16
16
 
17
+ const translations = {
18
+ de: {
19
+ choosePromotion: "Bauernumwandlung wählen",
20
+ promotionDialogTitle: "Bauernumwandlung",
21
+ pieces: {q: "Dame", r: "Turm", b: "Läufer", n: "Springer"},
22
+ promoteTo: "Umwandeln in"
23
+ },
24
+ en: {
25
+ choosePromotion: "Choose promotion piece",
26
+ promotionDialogTitle: "Pawn promotion",
27
+ pieces: {q: "Queen", r: "Rook", b: "Bishop", n: "Knight"},
28
+ promoteTo: "Promote to"
29
+ }
30
+ }
31
+
17
32
  export const PROMOTION_DIALOG_RESULT_TYPE = {
18
33
  pieceSelected: "pieceSelected",
19
34
  canceled: "canceled"
@@ -22,12 +37,39 @@ export const PROMOTION_DIALOG_RESULT_TYPE = {
22
37
  export class PromotionDialog extends Extension {
23
38
 
24
39
  /** @constructor */
25
- constructor(chessboard) {
40
+ constructor(chessboard, props = {}) {
26
41
  super(chessboard)
42
+ this.props = {
43
+ language: navigator.language.substring(0, 2).toLowerCase()
44
+ }
45
+ Object.assign(this.props, props)
46
+ if (this.props.language !== "de" && this.props.language !== "en") {
47
+ this.props.language = "en"
48
+ }
49
+ this.t = translations[this.props.language]
50
+ this.pieceOrder = ["q", "r", "b", "n"]
51
+ this.focusedIndex = 0
52
+ this.previouslyFocusedElement = null
53
+
27
54
  this.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, this.extensionPointRedrawBoard.bind(this))
55
+ this.registerExtensionPoint(EXTENSION_POINT.destroy, this.destroy.bind(this))
28
56
  chessboard.showPromotionDialog = this.showPromotionDialog.bind(this)
29
57
  chessboard.isPromotionDialogShown = this.isPromotionDialogShown.bind(this)
30
- this.promotionDialogGroup = Svg.addElement(chessboard.view.interactiveTopLayer, "g", {class: "promotion-dialog-group"})
58
+ this.promotionDialogGroup = Svg.addElement(chessboard.view.interactiveTopLayer, "g", {
59
+ class: "promotion-dialog-group",
60
+ role: "dialog",
61
+ "aria-modal": "true",
62
+ "aria-label": this.t.choosePromotion
63
+ })
64
+
65
+ // Create live region for announcements
66
+ this.liveRegion = document.createElement("div")
67
+ this.liveRegion.setAttribute("aria-live", "polite")
68
+ this.liveRegion.setAttribute("aria-atomic", "true")
69
+ this.liveRegion.className = "cm-chessboard-promotion-live-region visually-hidden"
70
+ this.liveRegion.style.cssText = "position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"
71
+ chessboard.context.appendChild(this.liveRegion)
72
+
31
73
  this.state = {
32
74
  displayState: DISPLAY_STATE.hidden,
33
75
  callback: null,
@@ -36,10 +78,15 @@ export class PromotionDialog extends Extension {
36
78
  color: null
37
79
  }
38
80
  }
81
+
82
+ // Bind keyboard handler
83
+ this.handleKeyDown = this.handleKeyDown.bind(this)
39
84
  }
40
85
 
41
86
  // public (chessboard.showPromotionDialog)
42
87
  showPromotionDialog(square, color, callback) {
88
+ this.previouslyFocusedElement = document.activeElement
89
+ this.focusedIndex = 0
43
90
  this.state.dialogParams.square = square
44
91
  this.state.dialogParams.color = color
45
92
  this.state.callback = callback
@@ -47,6 +94,8 @@ export class PromotionDialog extends Extension {
47
94
  setTimeout(() => {
48
95
  this.chessboard.view.positionsAnimationTask.then(() => {
49
96
  this.setDisplayState(DISPLAY_STATE.shown)
97
+ this.announce(this.t.choosePromotion + ": " +
98
+ this.pieceOrder.map(p => this.t.pieces[p]).join(", "))
50
99
  })
51
100
  }
52
101
  )
@@ -63,16 +112,26 @@ export class PromotionDialog extends Extension {
63
112
  this.redrawDialog()
64
113
  }
65
114
 
66
- drawPieceButton(piece, point) {
115
+ drawPieceButton(piece, point, index) {
67
116
  const squareWidth = this.chessboard.view.squareWidth
68
117
  const squareHeight = this.chessboard.view.squareHeight
69
- Svg.addElement(this.promotionDialogGroup,
118
+ const pieceType = piece.charAt(1)
119
+ const pieceName = this.t.pieces[pieceType]
120
+ const buttonGroup = Svg.addElement(this.promotionDialogGroup, "g", {
121
+ class: "promotion-dialog-button-group",
122
+ role: "button",
123
+ tabindex: index === 0 ? "0" : "-1",
124
+ "aria-label": pieceName,
125
+ "data-piece": piece,
126
+ "data-index": index
127
+ })
128
+ Svg.addElement(buttonGroup,
70
129
  "rect", {
71
130
  x: point.x, y: point.y, width: squareWidth, height: squareHeight,
72
131
  class: "promotion-dialog-button",
73
132
  "data-piece": piece
74
133
  })
75
- this.chessboard.view.drawPiece(this.promotionDialogGroup, piece, point)
134
+ this.chessboard.view.drawPiece(buttonGroup, piece, point)
76
135
  }
77
136
 
78
137
  redrawDialog() {
@@ -106,57 +165,69 @@ export class PromotionDialog extends Extension {
106
165
  this.drawPieceButton(PIECE[dialogParams.color + "q"], {
107
166
  x: squareCenterPoint.x + offsetX,
108
167
  y: squareCenterPoint.y - squareHeight
109
- })
168
+ }, 0)
110
169
  this.drawPieceButton(PIECE[dialogParams.color + "r"], {
111
170
  x: squareCenterPoint.x + offsetX,
112
171
  y: squareCenterPoint.y - squareHeight * 2
113
- })
172
+ }, 1)
114
173
  this.drawPieceButton(PIECE[dialogParams.color + "b"], {
115
174
  x: squareCenterPoint.x + offsetX,
116
175
  y: squareCenterPoint.y - squareHeight * 3
117
- })
176
+ }, 2)
118
177
  this.drawPieceButton(PIECE[dialogParams.color + "n"], {
119
178
  x: squareCenterPoint.x + offsetX,
120
179
  y: squareCenterPoint.y - squareHeight * 4
121
- })
180
+ }, 3)
122
181
  } else {
123
182
  this.drawPieceButton(PIECE[dialogParams.color + "q"], {
124
183
  x: squareCenterPoint.x + offsetX,
125
184
  y: squareCenterPoint.y
126
- })
185
+ }, 0)
127
186
  this.drawPieceButton(PIECE[dialogParams.color + "r"], {
128
187
  x: squareCenterPoint.x + offsetX,
129
188
  y: squareCenterPoint.y + squareHeight
130
- })
189
+ }, 1)
131
190
  this.drawPieceButton(PIECE[dialogParams.color + "b"], {
132
191
  x: squareCenterPoint.x + offsetX,
133
192
  y: squareCenterPoint.y + squareHeight * 2
134
- })
193
+ }, 2)
135
194
  this.drawPieceButton(PIECE[dialogParams.color + "n"], {
136
195
  x: squareCenterPoint.x + offsetX,
137
196
  y: squareCenterPoint.y + squareHeight * 3
138
- })
197
+ }, 3)
139
198
  }
140
199
  }
141
200
  }
142
201
 
143
202
  promotionDialogOnClickPiece(event) {
144
203
  if (event.button !== 2) {
145
- if (event.target.dataset.piece) {
146
- if(this.state.callback) {
147
- this.state.callback({
148
- type: PROMOTION_DIALOG_RESULT_TYPE.pieceSelected,
149
- square: this.state.dialogParams.square,
150
- piece: event.target.dataset.piece
151
- })
204
+ // Find piece data from target or parent button group
205
+ let piece = event.target.dataset.piece
206
+ if (!piece && event.target.closest) {
207
+ const buttonGroup = event.target.closest(".promotion-dialog-button-group")
208
+ if (buttonGroup) {
209
+ piece = buttonGroup.dataset.piece
152
210
  }
153
- this.setDisplayState(DISPLAY_STATE.hidden)
211
+ }
212
+ if (piece) {
213
+ this.selectPiece(piece)
154
214
  } else {
155
215
  this.promotionDialogOnCancel(event)
156
216
  }
157
217
  }
158
218
  }
159
219
 
220
+ selectPiece(piece) {
221
+ if (this.state.callback) {
222
+ this.state.callback({
223
+ type: PROMOTION_DIALOG_RESULT_TYPE.pieceSelected,
224
+ square: this.state.dialogParams.square,
225
+ piece: piece
226
+ })
227
+ }
228
+ this.setDisplayState(DISPLAY_STATE.hidden)
229
+ }
230
+
160
231
  promotionDialogOnCancel(event) {
161
232
  if (this.state.displayState === DISPLAY_STATE.shown) {
162
233
  event.preventDefault()
@@ -184,11 +255,98 @@ export class PromotionDialog extends Extension {
184
255
  this.promotionDialogOnClickPiece.bind(this))
185
256
  this.contextMenuListener = this.contextMenu.bind(this)
186
257
  this.chessboard.view.svg.addEventListener("contextmenu", this.contextMenuListener)
258
+ // Add keyboard listener
259
+ document.addEventListener("keydown", this.handleKeyDown)
187
260
  } else if (displayState === DISPLAY_STATE.hidden) {
188
261
  this.clickDelegate.remove()
189
262
  this.chessboard.view.svg.removeEventListener("contextmenu", this.contextMenuListener)
263
+ // Remove keyboard listener
264
+ document.removeEventListener("keydown", this.handleKeyDown)
265
+ // Restore focus
266
+ if (this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
267
+ this.previouslyFocusedElement.focus()
268
+ }
190
269
  }
191
270
  this.redrawDialog()
271
+ // Focus first button after redraw when shown
272
+ if (displayState === DISPLAY_STATE.shown) {
273
+ setTimeout(() => {
274
+ this.focusButton(0)
275
+ }, 0)
276
+ }
277
+ }
278
+
279
+ handleKeyDown(event) {
280
+ if (this.state.displayState !== DISPLAY_STATE.shown) {
281
+ return
282
+ }
283
+ switch (event.key) {
284
+ case "ArrowDown":
285
+ case "ArrowRight":
286
+ event.preventDefault()
287
+ this.focusedIndex = (this.focusedIndex + 1) % 4
288
+ this.focusButton(this.focusedIndex)
289
+ break
290
+ case "ArrowUp":
291
+ case "ArrowLeft":
292
+ event.preventDefault()
293
+ this.focusedIndex = (this.focusedIndex - 1 + 4) % 4
294
+ this.focusButton(this.focusedIndex)
295
+ break
296
+ case "Enter":
297
+ case " ":
298
+ event.preventDefault()
299
+ const buttons = this.promotionDialogGroup.querySelectorAll(".promotion-dialog-button-group")
300
+ if (buttons[this.focusedIndex]) {
301
+ const piece = buttons[this.focusedIndex].dataset.piece
302
+ this.selectPiece(piece)
303
+ }
304
+ break
305
+ case "Escape":
306
+ event.preventDefault()
307
+ this.setDisplayState(DISPLAY_STATE.hidden)
308
+ if (this.state.callback) {
309
+ this.state.callback({type: PROMOTION_DIALOG_RESULT_TYPE.canceled})
310
+ }
311
+ break
312
+ case "Tab":
313
+ // Trap focus within dialog
314
+ event.preventDefault()
315
+ if (event.shiftKey) {
316
+ this.focusedIndex = (this.focusedIndex - 1 + 4) % 4
317
+ } else {
318
+ this.focusedIndex = (this.focusedIndex + 1) % 4
319
+ }
320
+ this.focusButton(this.focusedIndex)
321
+ break
322
+ }
323
+ }
324
+
325
+ focusButton(index) {
326
+ const buttons = this.promotionDialogGroup.querySelectorAll(".promotion-dialog-button-group")
327
+ buttons.forEach((btn, i) => {
328
+ btn.setAttribute("tabindex", i === index ? "0" : "-1")
329
+ })
330
+ if (buttons[index]) {
331
+ buttons[index].focus()
332
+ const pieceType = this.pieceOrder[index]
333
+ this.announce(this.t.pieces[pieceType])
334
+ }
335
+ }
336
+
337
+ announce(message) {
338
+ this.liveRegion.textContent = ""
339
+ // Small delay to ensure screen readers pick up the change
340
+ setTimeout(() => {
341
+ this.liveRegion.textContent = message
342
+ }, 50)
343
+ }
344
+
345
+ destroy() {
346
+ document.removeEventListener("keydown", this.handleKeyDown)
347
+ if (this.liveRegion && this.liveRegion.parentNode) {
348
+ this.liveRegion.parentNode.removeChild(this.liveRegion)
349
+ }
192
350
  }
193
351
 
194
352
  }