@pocketprep/ui-kit 3.6.0 → 3.7.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.
@@ -72,6 +72,7 @@
72
72
  import { ref } from 'vue'
73
73
  import { dark as vDark } from '../../directives'
74
74
  import RadioButton from './RadioButton.vue'
75
+ import { stripHtmlTags } from '../../utils'
75
76
 
76
77
  interface IItem {
77
78
  value: string | number
@@ -115,14 +116,6 @@ const keyPressedItem = (e: KeyboardEvent) => {
115
116
  }
116
117
  }
117
118
 
118
- const stripHtmlTags = (string?: string) => {
119
- if (string) {
120
- const div = document.createElement('div')
121
- div.innerHTML = string
122
- return div.textContent || ''
123
- }
124
- return ''
125
- }
126
119
  </script>
127
120
 
128
121
  <style lang="scss">
@@ -12,10 +12,10 @@
12
12
  {{ announcementMessage }}
13
13
  </div>
14
14
  <div
15
- v-breakpoint="breakpointsWithEl"
16
15
  class="uikit-question-build-list-choices-container-wrapper"
17
16
  >
18
17
  <TransitionGroup
18
+ v-dark="isDarkMode"
19
19
  v-breakpoint="breakpointsWithEl"
20
20
  name="list"
21
21
  tag="ol"
@@ -27,6 +27,7 @@
27
27
  (showBuildListOrder || reviewMode) && !isBuildListOrderCorrect,
28
28
  'uikit-question-build-list-choices-container--teach-group-review':
29
29
  (showBuildListOrder || reviewMode) && isTeachGroupReview,
30
+ 'uikit-question-build-list-choices-container--review-mode': reviewMode,
30
31
  }"
31
32
  role="list"
32
33
  :aria-label="`Build list with ${orderedChoices.length} items`"
@@ -46,6 +47,9 @@
46
47
  v-dark="isDarkMode"
47
48
  v-breakpoint="breakpointsWithEl"
48
49
  class="uikit-question-build-list-choices-container__choice-number"
50
+ :class="{
51
+ 'uikit-question-build-list-choices-container__choice-number--review-mode': reviewMode
52
+ }"
49
53
  >
50
54
  {{ index + 1 }}
51
55
  </div>
@@ -54,14 +58,14 @@
54
58
  v-breakpoint="breakpointsWithEl"
55
59
  class="uikit-question-build-list-choices-container__choice"
56
60
  :class="{
57
- 'uikit-question-build-list-choices-container__choice--showing-answer':
58
- reviewMode || showBuildListOrder
61
+ 'uikit-question-build-list-choices-container__choice--review-mode': reviewMode
59
62
  }"
60
63
  role="group"
61
64
  :aria-label="`Choice ${index + 1}: ${stripHtmlTags(choice.text || '')}`"
62
- :tabindex="(showBuildListOrder || reviewMode) ? -1 : 0"
65
+ :tabindex="-1"
63
66
  @keydown="handleCardKeydown($event, index)"
64
- @click="handleCardClick($event)"
67
+ @click.prevent
68
+ @mousedown.prevent
65
69
  :ref="(el) => setCardRef(el, choice.key)"
66
70
  >
67
71
  <div
@@ -72,7 +76,9 @@
72
76
  'uikit-question-build-list-choices-container__mobile-choice-number--correct':
73
77
  (reviewMode || showBuildListOrder) && isChoiceInCorrectPosition(choice, index),
74
78
  'uikit-question-build-list-choices-container__mobile-choice-number--incorrect':
75
- (reviewMode || showBuildListOrder) && !isChoiceInCorrectPosition(choice, index)
79
+ (reviewMode || showBuildListOrder) && !isChoiceInCorrectPosition(choice, index),
80
+ 'uikit-question-build-list-choices-container__mobile-choice-number--review-mode':
81
+ reviewMode,
76
82
  }"
77
83
  >
78
84
  {{ index + 1 }}
@@ -92,7 +98,7 @@
92
98
  {{ getCorrectPositionOrderNumber(choice) }}
93
99
  </div>
94
100
  <Icon
95
- v-if="isChoiceInCorrectPosition(choice, index)"
101
+ v-if="isChoiceInCorrectPosition(choice, index) && !reviewMode"
96
102
  v-dark="isDarkMode"
97
103
  v-breakpoint="breakpointsWithEl"
98
104
  class="uikit-question-build-list-choices-container__answer-correct-icon"
@@ -100,7 +106,7 @@
100
106
 
101
107
  />
102
108
  <Icon
103
- v-else
109
+ v-else-if="!isChoiceInCorrectPosition(choice, index) && !reviewMode"
104
110
  v-dark="isDarkMode"
105
111
  v-breakpoint="breakpointsWithEl"
106
112
  class="uikit-question-build-list-choices-container__answer-incorrect-icon"
@@ -119,7 +125,8 @@
119
125
  index === 0
120
126
  }"
121
127
  :disabled="index === 0"
122
- @click="moveChoiceUp(index)"
128
+ :tabindex="(showBuildListOrder || reviewMode) ? -1 : 0"
129
+ @click.prevent.stop="moveChoiceUp(index)"
123
130
  :aria-label="`Move '${stripHtmlTags(choice.text || '')}' up`"
124
131
  :title="'Move up'"
125
132
  >
@@ -138,7 +145,8 @@
138
145
  index === orderedChoices.length - 1
139
146
  }"
140
147
  :disabled="index === orderedChoices.length - 1"
141
- @click="moveChoiceDown(index)"
148
+ :tabindex="(showBuildListOrder || reviewMode) ? -1 : 0"
149
+ @click.prevent.stop="moveChoiceDown(index)"
142
150
  :aria-label="`Move '${stripHtmlTags(choice.text || '')}' down`"
143
151
  :title="'Move down'"
144
152
  >
@@ -163,6 +171,7 @@ import Icon from '../../Icons/Icon.vue'
163
171
  import { dark as vDark, breakpoint as vBreakpoint } from '../../../directives'
164
172
  import { useQuestionContext } from './composables'
165
173
  import type { TBuildListChoice } from '../question'
174
+ import { stripHtmlTags } from '../../../utils'
166
175
 
167
176
  const emit = defineEmits<{
168
177
  'reorderBuildList': [ choices: TBuildListChoice[] ]
@@ -186,6 +195,7 @@ const orderedChoices = ref<TBuildListChoice[]>([])
186
195
  const floatingChoiceKey = ref<string | null>(null)
187
196
  const choiceRefs = ref<Map<string, HTMLDivElement>>(new Map())
188
197
  const announcementMessage = ref<string>('')
198
+ const isMoveTriggeredByArrowKey = ref<boolean>(false)
189
199
 
190
200
  onMounted(() => {
191
201
  updateOrderedChoices()
@@ -196,15 +206,6 @@ const prefersReducedMotion = () => {
196
206
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches
197
207
  }
198
208
 
199
- const stripHtmlTags = (string?: string) => {
200
- if (string) {
201
- const div = document.createElement('div')
202
- div.innerHTML = string
203
- return div.textContent || ''
204
- }
205
- return ''
206
- }
207
-
208
209
  const updateOrderedChoices = () => {
209
210
  // Map the selectedBuildListChoiceOrder keys to full TBuildListChoice objects
210
211
  if (selectedBuildListChoiceOrder.value.length) {
@@ -238,11 +239,37 @@ const restoreFocusAfterMove = (movedChoiceKey: string) => {
238
239
  setTimeout(() => {
239
240
  const choiceEl = choiceRefs.value.get(movedChoiceKey)
240
241
  if (choiceEl) {
242
+ // Temporarily make the choice focusable for animation
243
+ choiceEl.setAttribute('tabindex', '0')
241
244
  choiceEl.focus()
245
+
246
+ // Remove tabindex after animation completes
247
+ const animationDelay = prefersReducedMotion() ? 0 : 350
248
+ setTimeout(() => {
249
+ choiceEl.setAttribute('tabindex', '-1')
250
+ }, animationDelay)
242
251
  }
243
252
  }, delay)
244
253
  }
245
254
 
255
+ const removeFocusAfterAnimation = (movedChoiceKey: string) => {
256
+ // Don't remove focus if we're in review mode or showing the build list order
257
+ if (showBuildListOrder.value || reviewMode.value) {
258
+ return
259
+ }
260
+
261
+ // Only remove focus if the move was NOT triggered by arrow keys
262
+ if (!isMoveTriggeredByArrowKey.value) {
263
+ const animationDelay = prefersReducedMotion() ? 0 : 350 // Slightly after animation completes
264
+ setTimeout(() => {
265
+ const choiceEl = choiceRefs.value.get(movedChoiceKey)
266
+ if (choiceEl) {
267
+ choiceEl.blur()
268
+ }
269
+ }, animationDelay)
270
+ }
271
+ }
272
+
246
273
  // For accessibility, we want to announce to the user where they moved the choice
247
274
  // to in the ordered list. ie: “Moved Photosynthesis to position 2 of 6.”
248
275
  const announceMove = (choiceText: string, newPosition: number, totalItems: number) => {
@@ -269,8 +296,6 @@ const moveChoiceUp = (index: number) => {
269
296
  newChoices[ index ] = previousItem
270
297
  newChoices[ index - 1 ] = currentItem
271
298
  orderedChoices.value = newChoices
272
- emit('reorderBuildList', newChoices)
273
-
274
299
  // Announce the move
275
300
  const choiceText = stripHtmlTags(currentItem.text || '')
276
301
  announceMove(choiceText, index, orderedChoices.value.length)
@@ -278,6 +303,9 @@ const moveChoiceUp = (index: number) => {
278
303
  // Restore focus to the moved card
279
304
  restoreFocusAfterMove(currentItem.key)
280
305
 
306
+ // Remove focus after animation if not triggered by arrow key
307
+ removeFocusAfterAnimation(currentItem.key)
308
+
281
309
  // Clear floating state after animation completes
282
310
  const animationDelay = prefersReducedMotion() ? 0 : 300
283
311
  setTimeout(() => {
@@ -300,7 +328,6 @@ const moveChoiceDown = (index: number) => {
300
328
  newChoices[ index ] = nextItem
301
329
  newChoices[ index + 1 ] = currentItem
302
330
  orderedChoices.value = newChoices
303
- emit('reorderBuildList', newChoices)
304
331
 
305
332
  // Announce the move
306
333
  const choiceText = stripHtmlTags(currentItem.text || '')
@@ -309,6 +336,9 @@ const moveChoiceDown = (index: number) => {
309
336
  // Restore focus to the moved card
310
337
  restoreFocusAfterMove(currentItem.key)
311
338
 
339
+ // Remove focus after animation
340
+ removeFocusAfterAnimation(currentItem.key)
341
+
312
342
  // Clear floating state after animation completes
313
343
  const animationDelay = prefersReducedMotion() ? 0 : 300
314
344
  setTimeout(() => {
@@ -332,27 +362,39 @@ const isChoiceInCorrectPosition = (choice: TBuildListChoice, currentIndex: numbe
332
362
  return correctOrderPosition === currentOrderPosition
333
363
  }
334
364
 
335
- const handleCardClick = (event: MouseEvent) => {
336
- if (showBuildListOrder.value || reviewMode.value) {
337
- event.preventDefault()
338
- event.stopPropagation()
339
- }
340
- }
341
-
342
365
  const handleCardKeydown = (event: KeyboardEvent, index: number) => {
343
366
  // Handle Alt+Arrow shortcuts for moving cards
344
367
  if (event.altKey) {
345
368
  if (event.key === 'ArrowUp' || event.key === 'Up') {
346
369
  event.preventDefault()
370
+ isMoveTriggeredByArrowKey.value = true
347
371
  moveChoiceUp(index)
372
+ // Reset flag after a short delay
373
+ setTimeout(() => {
374
+ isMoveTriggeredByArrowKey.value = false
375
+ }, 100)
348
376
  } else if (event.key === 'ArrowDown' || event.key === 'Down') {
349
377
  event.preventDefault()
378
+ isMoveTriggeredByArrowKey.value = true
350
379
  moveChoiceDown(index)
380
+ // Reset flag after a short delay
381
+ setTimeout(() => {
382
+ isMoveTriggeredByArrowKey.value = false
383
+ }, 100)
351
384
  }
352
385
  }
353
386
  }
354
387
 
355
- watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
388
+ watch(orderedChoices, () => {
389
+ const newChoices = orderedChoices.value
390
+ emit('reorderBuildList', newChoices)
391
+ }, { deep: true })
392
+
393
+ watch(selectedBuildListChoiceOrder, () => {
394
+ if ((reviewMode.value || showBuildListOrder.value)) {
395
+ updateOrderedChoices()
396
+ }
397
+ })
356
398
  </script>
357
399
 
358
400
  <style lang="scss">
@@ -384,22 +426,23 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
384
426
  flex-direction: column;
385
427
  gap: 12px;
386
428
  list-style: none;
387
- margin: 0;
429
+ margin: 0 0 0 -46px;
388
430
  padding: 0;
389
431
 
390
432
  &::after {
391
- content: '';
392
- position: absolute;
393
- top: -8px;
394
- bottom: -8px;
395
- left: 32px;
396
- right: -8px;
397
- border-radius: 11px;
398
- pointer-events: none;
433
+ content: '';
434
+ position: absolute;
435
+ top: -8px;
436
+ bottom: -8px;
437
+ left: 8px;
438
+ right: 10px;
439
+ border-radius: 11px;
440
+ pointer-events: none;
399
441
 
400
- }
442
+ }
401
443
 
402
444
  &--mobile {
445
+ margin: 0;
403
446
  &::after {
404
447
  content: '';
405
448
  position: absolute;
@@ -413,6 +456,17 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
413
456
  }
414
457
  }
415
458
 
459
+ &--review-mode:not(&--mobile) {
460
+ margin: 0;
461
+ max-width: 492px;
462
+ width: 100%;
463
+
464
+ &::after {
465
+ left: -8px;
466
+ right: -8px;
467
+ }
468
+ }
469
+
416
470
  &--correct {
417
471
  &::after {
418
472
  display: block;
@@ -456,8 +510,12 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
456
510
  z-index: 10;
457
511
 
458
512
  .uikit-question-build-list-choices-container__choice {
459
- box-shadow: 0 8px 24px 0 rgba(71, 89, 103, 0.4);
460
- border-radius: 8px;
513
+ outline: 2px solid $brand-blue;
514
+ outline-offset: 2px;
515
+
516
+ &--dark {
517
+ outline-color: $banana-bread;
518
+ }
461
519
  }
462
520
  }
463
521
  }
@@ -465,7 +523,7 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
465
523
  &__choice-number,
466
524
  &__mobile-choice-number {
467
525
  display: flex;
468
- align-items: center;
526
+ align-items: flex-end;
469
527
  justify-content: center;
470
528
  width: 24px;
471
529
  height: 24px;
@@ -483,13 +541,14 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
483
541
  }
484
542
 
485
543
  &__choice-number {
544
+ &--review-mode,
486
545
  &--mobile {
487
546
  display: none;
488
547
  }
489
548
  }
490
549
 
491
550
  &__mobile-choice-number {
492
- &:not(&--mobile) {
551
+ &:not(&--mobile, &--review-mode) {
493
552
  display: none;
494
553
  }
495
554
 
@@ -530,28 +589,15 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
530
589
  box-shadow: 0 1px 4px 0 rgba(71, 89, 103, 0.30);
531
590
  outline: none;
532
591
 
533
- &:focus:not(&--showing-answer) {
534
- outline: 2px solid $brand-blue;
535
- outline-offset: 2px;
536
- }
537
-
538
592
  &--dark {
539
593
  border: 1px solid $slate;
540
594
  background-color: $brand-black;
541
595
  box-shadow: 0 1px 4px 0 rgba(71, 89, 103, 0.30);
542
-
543
- &:focus:not(&--showing-answer) {
544
- outline-color: $banana-bread;
545
- }
546
596
  }
547
597
 
548
598
  &--mobile {
549
599
  width: 100%;
550
600
  }
551
-
552
- &--showing-answer {
553
- gap: 27px;
554
- }
555
601
  }
556
602
 
557
603
  &__choice-text {
@@ -595,9 +641,30 @@ watch(selectedBuildListChoiceOrder, updateOrderedChoices, { immediate: true })
595
641
  cursor: pointer;
596
642
  transition: all 0.2s ease;
597
643
 
644
+ &:focus {
645
+ outline: none;
646
+ border: 2px solid $white;
647
+ box-shadow: 0 0 0 2px $brand-blue-hover;
648
+ }
649
+
650
+ &:hover,
651
+ &:focus {
652
+ background-color: $brand-blue-hover;
653
+ }
654
+
598
655
  &--dark {
599
656
  background-color: $banana-bread;
600
657
  color: $brand-black;
658
+
659
+ &:focus {
660
+ border: 2px solid $brand-black;
661
+ box-shadow: 0 0 0 2px $butterscotch;
662
+ }
663
+
664
+ &:hover,
665
+ &:focus {
666
+ background-color: $butterscotch;
667
+ }
601
668
  }
602
669
 
603
670
  &--disabled {
@@ -277,6 +277,7 @@ import PocketButton from '../../Buttons/Button.vue'
277
277
  import { dark as vDark, breakpoint as vBreakpoint } from '../../../directives'
278
278
  import { useQuestionContext } from './composables'
279
279
  import type { TChoiceKey } from '../question'
280
+ import { stripHtmlTags } from '../../../utils'
280
281
 
281
282
  const emit = defineEmits<{
282
283
  'emitChoiceFocusIn': [ choiceKey: TChoiceKey ]
@@ -318,15 +319,6 @@ const {
318
319
  breakpointsWithEl,
319
320
  } = useQuestionContext()
320
321
 
321
- const stripHtmlTags = (string?: string) => {
322
- if (string) {
323
- const div = document.createElement('div')
324
- div.innerHTML = string
325
- return div.textContent || ''
326
- }
327
- return ''
328
- }
329
-
330
322
  const emitChoiceFocusIn = (choiceKey: TChoiceKey) => {
331
323
  emit('emitChoiceFocusIn', choiceKey)
332
324
  }