@pocketprep/ui-kit 3.7.1 → 3.7.3

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.
@@ -30,7 +30,7 @@
30
30
  'uikit-question-build-list-choices-container--review-mode': reviewMode,
31
31
  }"
32
32
  role="list"
33
- :aria-label="`Build list with ${orderedChoices.length} items`"
33
+ :aria-label="`List with ${orderedChoices.length} items`"
34
34
  >
35
35
  <li
36
36
  v-for="(choice, index) in orderedChoices"
@@ -60,10 +60,9 @@
60
60
  :class="{
61
61
  'uikit-question-build-list-choices-container__choice--review-mode': reviewMode
62
62
  }"
63
- role="group"
64
- :aria-label="`Choice ${index + 1}: ${stripHtmlTags(choice.text || '')}`"
65
63
  :tabindex="-1"
66
- @keydown="handleCardKeydown($event, index)"
64
+ @keydown.enter.stop="handleCardKeydown($event, index)"
65
+ @keydown.space.stop="handleCardKeydown($event, index)"
67
66
  @click.prevent
68
67
  @mousedown.prevent
69
68
  :ref="(el) => setCardRef(el, choice.key)"
@@ -125,7 +124,7 @@
125
124
  index === 0
126
125
  }"
127
126
  :disabled="index === 0"
128
- :tabindex="(showBuildListOrder || reviewMode) ? -1 : 0"
127
+ :tabindex="buttonsAreFocusable && index !== 0 ? 0 : -1"
129
128
  @click.prevent.stop="moveChoiceUp(index)"
130
129
  :aria-label="`Move '${stripHtmlTags(choice.text || '')}' up`"
131
130
  :title="'Move up'"
@@ -145,7 +144,7 @@
145
144
  index === orderedChoices.length - 1
146
145
  }"
147
146
  :disabled="index === orderedChoices.length - 1"
148
- :tabindex="(showBuildListOrder || reviewMode) ? -1 : 0"
147
+ :tabindex="buttonsAreFocusable && index !== orderedChoices.length - 1 ? 0 : -1"
149
148
  @click.prevent.stop="moveChoiceDown(index)"
150
149
  :aria-label="`Move '${stripHtmlTags(choice.text || '')}' down`"
151
150
  :title="'Move down'"
@@ -166,7 +165,7 @@
166
165
  </template>
167
166
 
168
167
  <script setup lang="ts">
169
- import { ref, watch, onMounted, type ComponentPublicInstance } from 'vue'
168
+ import { ref, watch, onMounted, computed, type ComponentPublicInstance } from 'vue'
170
169
  import Icon from '../../Icons/Icon.vue'
171
170
  import { dark as vDark, breakpoint as vBreakpoint } from '../../../directives'
172
171
  import { useQuestionContext } from './composables'
@@ -197,6 +196,10 @@ const choiceRefs = ref<Map<string, HTMLDivElement>>(new Map())
197
196
  const announcementMessage = ref<string>('')
198
197
  const isMoveTriggeredByArrowKey = ref<boolean>(false)
199
198
 
199
+ const buttonsAreFocusable = computed(() => {
200
+ return !showBuildListOrder.value && !reviewMode.value
201
+ })
202
+
200
203
  onMounted(() => {
201
204
  updateOrderedChoices()
202
205
  })
@@ -435,7 +438,7 @@ watch(selectedBuildListChoiceOrder, () => {
435
438
  top: -8px;
436
439
  bottom: -8px;
437
440
  left: 8px;
438
- right: 10px;
441
+ right: 14px;
439
442
  border-radius: 11px;
440
443
  pointer-events: none;
441
444
 
@@ -736,12 +739,10 @@ watch(selectedBuildListChoiceOrder, () => {
736
739
 
737
740
  // TransitionGroup animations
738
741
  .list-move,
739
- .list-enter-active,
740
742
  .list-leave-active {
741
743
  transition: all 0.2s ease;
742
744
  }
743
745
 
744
- .list-enter-from,
745
746
  .list-leave-to {
746
747
  opacity: 0;
747
748
  transform: translateX(30px);
@@ -755,12 +756,10 @@ watch(selectedBuildListChoiceOrder, () => {
755
756
  // Respect prefers-reduced-motion
756
757
  @media (prefers-reduced-motion: reduce) {
757
758
  .list-move,
758
- .list-enter-active,
759
759
  .list-leave-active {
760
760
  transition: none;
761
761
  }
762
762
 
763
- .list-enter-from,
764
763
  .list-leave-to {
765
764
  opacity: 1;
766
765
  transform: none;
@@ -27,6 +27,7 @@ const {
27
27
  isDarkMode,
28
28
  showMPMCAnswers,
29
29
  selectedMPMCChoices,
30
+ mpmcChoices,
30
31
  } = useQuestionContext()
31
32
 
32
33
  const mpmcRadioGrid = ref<IMPMCRadioOptions[] | undefined>(undefined)
@@ -37,10 +38,7 @@ const mpmcLabels = computed(() => {
37
38
  })
38
39
 
39
40
  const questionChoices = computed(() => {
40
- const choices = question.value.choices
41
-
42
- // Shuffle mpmc choices
43
- return [ ...choices ].sort(() => Math.random() - 0.5)
41
+ return mpmcChoices.value
44
42
  })
45
43
 
46
44
  const choiceKeysByLabelIndexObj = computed(() => {
@@ -235,8 +233,9 @@ watch(selectedMPMCChoices, () => {
235
233
  :aria-label="
236
234
  `${stripHtmlTags(label)}. ${expandedPartNumbers.includes(labelIndex) ? 'Expanded' : 'Collapsed'}.`
237
235
  "
238
- @keydown.enter="openChoiceDropdown(labelIndex)"
239
- @keydown.space.prevent="openChoiceDropdown(labelIndex)"
236
+ @keydown.enter.stop="openChoiceDropdown(labelIndex)"
237
+ @keydown.space.stop="openChoiceDropdown(labelIndex)"
238
+ @mousedown.prevent
240
239
  >
241
240
  <div
242
241
  class="uikit-question-mpmc-choices-container__part"
@@ -25,6 +25,10 @@ const selectedChoice = defineModel<TChoiceKey | null>({ default: null })
25
25
 
26
26
  const selectChoice = (choiceKey: TChoiceKey) => {
27
27
  selectedChoice.value = choiceKey
28
+ // Remove focus from the choice after selecting a choice
29
+ if (document.activeElement instanceof HTMLElement) {
30
+ document.activeElement.blur()
31
+ }
28
32
  }
29
33
 
30
34
  const radioButtonColor = (choice: TChoiceKey) => {
@@ -130,16 +134,28 @@ const radioButtonColor = (choice: TChoiceKey) => {
130
134
  outline: none;
131
135
  transition: background-color 0.2s ease;
132
136
 
137
+ &:last-child {
138
+ border-radius: 0 0 5px 5px;
139
+ }
140
+
133
141
  &:not(&--show-answer) {
134
142
  &:focus {
135
143
  border: 0.5px solid $brand-blue;
136
144
  }
145
+
146
+ &:hover {
147
+ background: $pearl;
148
+ }
137
149
  }
138
150
 
139
151
  &--dark {
140
152
  &:focus:not(.uikit-mpmc-radio-group__option--show-answer) {
141
153
  border: 0.5px solid $banana-bread;
142
154
  }
155
+
156
+ &:hover:not(.uikit-mpmc-radio-group__option--show-answer) {
157
+ background-color: $moonlit-ocean;
158
+ }
143
159
  }
144
160
  }
145
161
 
@@ -30,8 +30,8 @@
30
30
  ? ` of ${quizLength}`
31
31
  : ''
32
32
  }${
33
- (showAnswers || showMatrixAnswers)
34
- ? isCorrect
33
+ (showAnswers || showMatrixAnswers || showBuildListOrder || showMPMCAnswers)
34
+ ? isCorrectlyAnswered
35
35
  ? ', Answered Correctly'
36
36
  : ', Answered Incorrectly'
37
37
  : ''
@@ -62,6 +62,7 @@
62
62
  import Icon from '../../Icons/Icon.vue'
63
63
  import { dark as vDark, breakpoint as vBreakpoint } from '../../../directives'
64
64
  import { useQuestionContext } from './composables'
65
+ import { computed } from 'vue'
65
66
 
66
67
  const {
67
68
  // questionEl is used by the breakpoint directive
@@ -72,15 +73,39 @@ const {
72
73
  questionNumber,
73
74
  isDarkMode,
74
75
  isCorrect,
76
+ isBuildListQuestion,
77
+ isMatrixQuestion,
78
+ isMPMCQuestion,
79
+ isBuildListOrderCorrect,
80
+ isMatrixQuestionCorrect,
81
+ isMPMCQuestionCorrect,
75
82
  contextIconType,
76
83
  showAnswers,
84
+ showBuildListOrder,
77
85
  showMatrixAnswers,
86
+ showMPMCAnswers,
78
87
  } = useQuestionContext()
79
88
 
80
89
  defineExpose({
81
90
  questionEl,
82
91
  })
83
92
 
93
+ const isCorrectlyAnswered = computed(() => {
94
+ if (isMatrixQuestion.value) {
95
+ return isMatrixQuestionCorrect.value
96
+ }
97
+
98
+ if (isBuildListQuestion) {
99
+ return isBuildListOrderCorrect.value
100
+ }
101
+
102
+ if (isMPMCQuestion.value) {
103
+ return isMPMCQuestionCorrect.value
104
+ }
105
+
106
+ return isCorrect.value
107
+ })
108
+
84
109
  </script>
85
110
 
86
111
  <style lang="scss">
@@ -219,7 +219,8 @@ const toggleSummaryExplanationImageLongAlt = () => {
219
219
  }
220
220
 
221
221
  &--build-list-question-not-review-mode {
222
- max-width: 452px;
222
+ max-width: 450px;
223
+ margin-left: -4px
223
224
  }
224
225
 
225
226
  &--matrix-question-review-mode#{&}--tablet-landscape,
@@ -229,6 +230,8 @@ const toggleSummaryExplanationImageLongAlt = () => {
229
230
 
230
231
  &--mpmc-question {
231
232
  max-width: 492px;
233
+ margin-left: 4px;
234
+ margin-top: 8px;
232
235
  }
233
236
 
234
237
  &--tablet-portrait {
@@ -1,6 +1,6 @@
1
1
  import { inject, ref } from 'vue'
2
2
  import * as InjectionKeys from './injectionSymbols'
3
- import type { TChoice, TChoiceKey, TMatrixChoiceKey, TBuildListChoice } from '../question'
3
+ import type { TChoice, TChoiceKey, TMatrixChoiceKey, TBuildListChoice, TMPMCChoice } from '../question'
4
4
 
5
5
  export const useQuestionContext = () => {
6
6
  const question = inject(InjectionKeys.questionKey)
@@ -16,6 +16,7 @@ export const useQuestionContext = () => {
16
16
  choiceScores,
17
17
  choices: inject(InjectionKeys.choicesKey, ref<TChoice[]>([])),
18
18
  buildListChoices: inject(InjectionKeys.buildListChoicesKey, ref<TBuildListChoice[]>([])),
19
+ mpmcChoices: inject(InjectionKeys.mpmcChoicesKey, ref<TMPMCChoice[]>([])),
19
20
  questionEl: inject(InjectionKeys.questionElKey, ref(null)),
20
21
  breakpointsWithEl: inject(InjectionKeys.breakpointsWithElKey, ref({
21
22
  breakpoints: {
@@ -11,12 +11,14 @@ import type {
11
11
  TMatrixChoiceScores,
12
12
  TBuildListChoiceScores,
13
13
  TQuizMode,
14
+ TMPMCChoice,
14
15
  } from '../question'
15
16
  import type { ComputedRef, InjectionKey, Ref } from 'vue'
16
17
 
17
18
  export const questionKey = Symbol('question') as InjectionKey<ComputedRef<Study.Class.QuestionJSON>>
18
19
  export const choicesKey = Symbol('choices') as InjectionKey<ComputedRef<TChoice[]>>
19
- export const buildListChoicesKey = Symbol('choices') as InjectionKey<ComputedRef<TBuildListChoice[]>>
20
+ export const buildListChoicesKey = Symbol('buildListChoices') as InjectionKey<ComputedRef<TBuildListChoice[]>>
21
+ export const mpmcChoicesKey = Symbol('mpmcChoices') as InjectionKey<ComputedRef<TMPMCChoice[]>>
20
22
  export const questionElKey = Symbol('questionEl') as InjectionKey<Ref<Element | null>>
21
23
  export const breakpointsWithElKey = Symbol('breakpointsWithEl') as InjectionKey<Ref<{
22
24
  breakpoints: {
@@ -374,7 +374,6 @@
374
374
  >
375
375
  Check Answer
376
376
  </PocketButton>
377
-
378
377
  <PocketButton
379
378
  v-else-if="(
380
379
  showAnswers
@@ -471,6 +470,7 @@ import type {
471
470
  TBuildListChoiceKey,
472
471
  TChoice,
473
472
  TBuildListChoice,
473
+ TMPMCChoice,
474
474
  TChoiceScores,
475
475
  TMatrixChoiceScores,
476
476
  TBuildListChoiceScores,
@@ -801,6 +801,10 @@ const buildListChoices = computed(() => {
801
801
  return shuffledChoices
802
802
  })
803
803
 
804
+ const mpmcChoices = computed(() => {
805
+ return shuffleMPMCChoices([ ...props.question.choices ])
806
+ })
807
+
804
808
  const isCorrect = computed(() => {
805
809
  // In order to be correct, user must have selected all the answers and none of the distractors
806
810
  return showAnswers.value
@@ -947,11 +951,11 @@ const isMatrixQuestionAnswered = computed(() => {
947
951
 
948
952
  const isMPMCQuestionAnswered = computed(() => {
949
953
  const mpmcLabels = question.value.mpmcLabels
950
- const mpmcChoices = question.value.choices
954
+ const mpmcQuestionChoices = question.value.choices
951
955
  const selectedMPMCLabelIndexes: number[] = []
952
956
 
953
957
  selectedMPMCChoices.value.forEach(choice => {
954
- const mpmcChoiceObj = mpmcChoices.find(c => c.id === choice)
958
+ const mpmcChoiceObj = mpmcQuestionChoices.find(c => c.id === choice)
955
959
  if (
956
960
  mpmcChoiceObj?.labelIndex !== undefined
957
961
  && !selectedMPMCLabelIndexes.includes(mpmcChoiceObj?.labelIndex)
@@ -988,47 +992,77 @@ const prompt = computed(() => {
988
992
  })
989
993
  })
990
994
 
995
+ const isTEIsQuestionType = computed(() => {
996
+ return isMPMCQuestion.value || isMatrixQuestion.value || isBuildListQuestion.value
997
+ })
998
+
991
999
  const keydownListener = (e: KeyboardEvent) => {
992
1000
  switch (e.code) {
993
1001
  case 'KeyA':
994
- choices.value[0] && selectChoice(choices.value[0].key, true)
1002
+ if (!isTEIsQuestionType.value) {
1003
+ choices.value[0] && selectChoice(choices.value[0].key, true)
1004
+ }
995
1005
  break
996
1006
  case 'KeyB':
997
- choices.value[1] && selectChoice(choices.value[1].key, true)
1007
+ if (!isTEIsQuestionType.value) {
1008
+ choices.value[1] && selectChoice(choices.value[1].key, true)
1009
+ }
998
1010
  break
999
1011
  case 'KeyC':
1000
- choices.value[2] && selectChoice(choices.value[2].key, true)
1012
+ if (!isTEIsQuestionType.value) {
1013
+ choices.value[2] && selectChoice(choices.value[2].key, true)
1014
+ }
1001
1015
  break
1002
1016
  case 'KeyD':
1003
- choices.value[3] && selectChoice(choices.value[3].key, true)
1017
+ if (!isTEIsQuestionType.value) {
1018
+ choices.value[3] && selectChoice(choices.value[3].key, true)
1019
+ }
1004
1020
  break
1005
1021
  case 'KeyE':
1006
- choices.value[4] && selectChoice(choices.value[4].key, true)
1022
+ if (!isTEIsQuestionType.value) {
1023
+ choices.value[4] && selectChoice(choices.value[4].key, true)
1024
+ }
1007
1025
  break
1008
1026
  case 'KeyF':
1009
- choices.value[5] && selectChoice(choices.value[5].key, true)
1027
+ if (!isTEIsQuestionType.value) {
1028
+ choices.value[5] && selectChoice(choices.value[5].key, true)
1029
+ }
1010
1030
  break
1011
1031
  case 'KeyG':
1012
- choices.value[6] && selectChoice(choices.value[6].key, true)
1032
+ if (!isTEIsQuestionType.value) {
1033
+ choices.value[6] && selectChoice(choices.value[6].key, true)
1034
+ }
1013
1035
  break
1014
1036
  case 'KeyH':
1015
- choices.value[7] && selectChoice(choices.value[7].key, true)
1037
+ if (!isTEIsQuestionType.value) {
1038
+ choices.value[7] && selectChoice(choices.value[7].key, true)
1039
+ }
1016
1040
  break
1017
1041
  case 'KeyI':
1018
- choices.value[8] && selectChoice(choices.value[8].key, true)
1042
+ if (!isTEIsQuestionType.value) {
1043
+ choices.value[8] && selectChoice(choices.value[8].key, true)
1044
+ }
1019
1045
  break
1020
1046
  case 'KeyJ':
1021
- choices.value[9] && selectChoice(choices.value[9].key, true)
1047
+ if (!isTEIsQuestionType.value) {
1048
+ choices.value[9] && selectChoice(choices.value[9].key, true)
1049
+ }
1022
1050
  break
1023
1051
  case 'KeyX':
1024
- showAnswers.value && toggleExplanation()
1052
+ (showAnswers.value || showMatrixAnswers.value || showBuildListOrder.value || showMPMCAnswers.value)
1053
+ && toggleExplanation()
1025
1054
  break
1026
1055
  case 'Escape':
1027
1056
  emitClose()
1028
1057
  e.preventDefault()
1029
1058
  break
1030
1059
  case 'Enter':
1031
- if (!showAnswers.value && selectedChoices.value.length && focusChoiceKey.value === null) {
1060
+ if (
1061
+ !showAnswers.value
1062
+ && selectedChoices.value.length
1063
+ && focusChoiceKey.value === null
1064
+ && !isTEIsQuestionType.value
1065
+ ) {
1032
1066
  clickCheckAnswer()
1033
1067
  e.preventDefault()
1034
1068
  }
@@ -1163,6 +1197,30 @@ const shuffleBuildListChoices = (choicesToShuffle: TBuildListChoice[]): TBuildLi
1163
1197
  : sortedChoices
1164
1198
  }
1165
1199
 
1200
+ const shuffleMPMCChoices = (choicesToShuffle: TMPMCChoice[]): TMPMCChoice[]=> {
1201
+ const sortedChoices = choicesToShuffle.sort((a, b) => {
1202
+ const hashChar = (char: string, num: number) => ((num << 5) - num) + char.charCodeAt(0)
1203
+
1204
+ const aHash = a.text?.split('')
1205
+ .reduce((acc: number, char: string) => hashChar(char, acc) & hashChar(char, acc), 0)
1206
+ const bHash = b.text?.split('')
1207
+ .reduce((acc: number, char: string) => hashChar(char, acc) & hashChar(char, acc), 0)
1208
+
1209
+ return (aHash || 0) - (bHash || 0)
1210
+ })
1211
+
1212
+ return props.answerSeed
1213
+ ? props.answerSeed.reduce<TMPMCChoice[]>((acc, i) => {
1214
+ const sortedChoice = sortedChoices[i]
1215
+ if (sortedChoice) {
1216
+ acc.push(sortedChoice)
1217
+ }
1218
+
1219
+ return acc
1220
+ }, [])
1221
+ : sortedChoices
1222
+ }
1223
+
1166
1224
  const choiceFocusOut = (event: FocusEvent) => {
1167
1225
  const relatedTarget = event.relatedTarget
1168
1226
  if (
@@ -1426,7 +1484,6 @@ const clickCheckMatrixAnswer = () => {
1426
1484
  const clickCheckMPMCAnswer = () => {
1427
1485
  if (!props.hideAnswer) {
1428
1486
  showMPMCAnswers.value = true
1429
-
1430
1487
  emitCheckAnswer({
1431
1488
  isCorrect: isMPMCQuestionCorrect.value,
1432
1489
  selectedChoices: selectedMPMCChoices.value,
@@ -1621,6 +1678,7 @@ onBeforeUnmount(() => {
1621
1678
  provide(InjectionKeys.questionKey, question)
1622
1679
  provide(InjectionKeys.choicesKey, choices)
1623
1680
  provide(InjectionKeys.buildListChoicesKey, buildListChoices)
1681
+ provide(InjectionKeys.mpmcChoicesKey, mpmcChoices)
1624
1682
  provide(InjectionKeys.questionElKey, questionEl)
1625
1683
  provide(InjectionKeys.breakpointsWithElKey, breakpointsWithEl)
1626
1684
  provide(InjectionKeys.quizLengthKey, quizLength)
@@ -17,6 +17,12 @@ export type TBreakPointsObject = {
17
17
 
18
18
  export type TChoice = { text?: string; key: TChoiceKey }
19
19
  export type TBuildListChoice = { text?: string; key: TBuildListChoiceKey }
20
+ export type TMPMCChoice = {
21
+ text?: string
22
+ id?: string
23
+ isCorrect?: boolean
24
+ labelIndex?: number
25
+ }
20
26
 
21
27
  export type TChoiceScores = Partial<Record<TChoiceKey, number>> & {
22
28
  totalAnswered: number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pocketprep/ui-kit",
3
- "version": "3.7.1",
3
+ "version": "3.7.3",
4
4
  "description": "Pocket Prep UI Kit",
5
5
  "author": "pocketprep",
6
6
  "scripts": {