@pocketprep/ui-kit 3.0.39 → 3.1.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.
@@ -52,24 +52,30 @@ export default class ModalContainer extends Vue {
52
52
 
53
53
  focusListener: Parameters<typeof addEventListener>[1] | null = null
54
54
  savedYPosition = 0
55
+ modalNumber = 0
56
+
57
+ numberOfModals () {
58
+ return document.querySelectorAll('.uikit-modal-container').length
59
+ }
55
60
 
56
61
  mounted () {
62
+ this.modalNumber = this.numberOfModals()
57
63
  // Prevent an error where multiple modals trigger a loop
58
64
  // TODO: Find a way to have only the latest modal's focusListener active
59
- const openModals = document.querySelectorAll('.uikit-modal-container')
60
- if (openModals.length === 1) {
61
- const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]'
65
+
66
+ const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]'
62
67
 
63
- // Reset focus to last element in modal so next tab will move it to top
64
- const modalContainerEl = this.$refs['modalContainer'] as HTMLElement
65
- const modalFocusableEls = Array.from<HTMLElement>(modalContainerEl.querySelectorAll(focusableSelectors))
66
- if (modalFocusableEls.length) {
67
- modalFocusableEls[modalFocusableEls.length - 1]?.focus()
68
- modalFocusableEls[modalFocusableEls.length - 1]?.blur()
69
- }
68
+ // Reset focus to last element in modal so next tab will move it to top
69
+ const modalContainerEl = this.$refs['modalContainer'] as HTMLElement
70
+ const modalFocusableEls = Array.from<HTMLElement>(modalContainerEl.querySelectorAll(focusableSelectors))
71
+ if (modalFocusableEls.length) {
72
+ modalFocusableEls[modalFocusableEls.length - 1]?.focus()
73
+ modalFocusableEls[modalFocusableEls.length - 1]?.blur()
74
+ }
70
75
 
71
- // Trap the user's focus within the modal - don't allow focusing elements behind the overlay
72
- this.focusListener = event => {
76
+ // Trap the user's focus within the modal - don't allow focusing elements behind the overlay
77
+ this.focusListener = event => {
78
+ if (this.modalNumber === this.numberOfModals()) { // Only focus on the last open panel
73
79
  const target = (event as FocusEvent).target as HTMLElement // The element receiving focus
74
80
  const isFocusOutside = target && modalContainerEl && !modalContainerEl.contains(target)
75
81
  const hasCalendarClass = target
@@ -90,20 +96,20 @@ export default class ModalContainer extends Vue {
90
96
  if (firstFocusableModalChild) {
91
97
  const relatedTarget = (event as FocusEvent).relatedTarget // The element last focused
92
98
  if (relatedTarget === firstFocusableModalChild && lastFocusableModalChild) {
93
- // If focus moves from first element -> outside modal, focus the last element instead
99
+ // If focus moves from first element -> outside modal, focus the last element instead
94
100
  lastFocusableModalChild.focus()
95
101
  } else if (relatedTarget === lastFocusableModalChild && firstFocusableModalChild) {
96
- // If focus moves from last element -> outside modal, focus the first element instead
102
+ // If focus moves from last element -> outside modal, focus the first element instead
97
103
  firstFocusableModalChild.focus()
98
104
  } else if (relatedTarget && relatedTarget instanceof HTMLElement) {
99
- // If focus goes outside in a different way, return focus to where it came from if possible
105
+ // If focus goes outside in a different way, return focus to where it came from if possible
100
106
  relatedTarget.focus()
101
107
  } else {
102
- // Otherwise, just return focus to the first element
108
+ // Otherwise, just return focus to the first element
103
109
  firstFocusableModalChild.focus()
104
110
  }
105
111
  } else {
106
- // If the modal doesn't have any focusable children, focus the container instead
112
+ // If the modal doesn't have any focusable children, focus the container instead
107
113
  if (modalContainerEl.tabIndex === -1) {
108
114
  modalContainerEl.tabIndex = 0
109
115
  }
@@ -111,8 +117,8 @@ export default class ModalContainer extends Vue {
111
117
  }
112
118
  }
113
119
  }
114
- document.addEventListener('focusin', this.focusListener)
115
120
  }
121
+ document.addEventListener('focusin', this.focusListener)
116
122
 
117
123
  // prevent scrolling outside of modal
118
124
  const openModalCount = Number(document.body.getAttribute('data-openModalCount'))
@@ -102,7 +102,7 @@
102
102
  :class="{
103
103
  'uikit-question__prompt--passage-and-image': question.passage || passageImageUrl,
104
104
  }"
105
- v-html="question.question"
105
+ v-html="question.prompt"
106
106
  />
107
107
  <PocketButton
108
108
  v-if="question.passage || passageImageUrl"
@@ -461,7 +461,7 @@
461
461
  'uikit-question__toggle-dropdown-explanation-img-description--open':
462
462
  showExplanationImageLongAlt,
463
463
  'uikit-question__toggle-dropdown-explanation-img-description--no-reference':
464
- !question.reference || hideReferences,
464
+ !reference || hideReferences,
465
465
  }"
466
466
  :is-dark-mode="isDarkMode"
467
467
  :aria-expanded="showExplanationImageLongAlt ? 'true' : 'false'"
@@ -490,12 +490,12 @@
490
490
  v-html="explanationImageLongAlt"
491
491
  />
492
492
  <div
493
- v-if="question.reference && !hideReferences"
493
+ v-if="reference && !hideReferences"
494
494
  v-dark="isDarkMode"
495
495
  class="uikit-question__dropdown-reference"
496
496
  >
497
497
  <span class="uikit-question__dropdown-reference-label">Reference: </span>
498
- <div v-html="question.reference" />
498
+ <div v-html="reference" />
499
499
  </div>
500
500
  </div>
501
501
  </div>
@@ -661,7 +661,7 @@
661
661
  'uikit-question__toggle-summary-dropdown-explanation-img-description--open':
662
662
  showExplanationImageLongAlt,
663
663
  'uikit-question__toggle-summary-dropdown-explanation-img-description--no-reference':
664
- !question.reference || hideReferences,
664
+ !reference || hideReferences,
665
665
  }"
666
666
  :is-dark-mode="isDarkMode"
667
667
  :aria-expanded="showExplanationImageLongAlt ? 'true' : 'false'"
@@ -690,12 +690,12 @@
690
690
  v-html="explanationImageLongAlt"
691
691
  />
692
692
  <div
693
- v-if="question.reference && !hideReferences"
693
+ v-if="reference && !hideReferences"
694
694
  v-dark="isDarkMode"
695
695
  class="uikit-question__summary-dropdown-reference"
696
696
  >
697
697
  <span class="uikit-question__summary-dropdown-reference-label">Reference: </span>
698
- <div v-html="question.reference" />
698
+ <div v-html="reference" />
699
699
  </div>
700
700
  </div>
701
701
  <Icon
@@ -924,12 +924,12 @@
924
924
  v-html="explanationImageLongAlt"
925
925
  />
926
926
  <div
927
- v-if="question.reference && !hideReferences"
927
+ v-if="reference && !hideReferences"
928
928
  v-dark="isDarkMode"
929
929
  class="uikit-question__reference"
930
930
  >
931
931
  <span class="uikit-question__reference-label">Reference: </span>
932
- <div v-html="question.reference" />
932
+ <div v-html="reference" />
933
933
  </div>
934
934
  <div
935
935
  v-if="!reviewMode"
@@ -1001,7 +1001,7 @@ type TViewNames = {
1001
1001
  },
1002
1002
  })
1003
1003
  export default class Question extends Vue {
1004
- @Prop() question!: Study.Class.ExamDataJSON
1004
+ @Prop() question!: Study.Class.QuestionJSON
1005
1005
  @Prop() questionNumber!: number
1006
1006
  @Prop() quizLength!: number
1007
1007
  @Prop({ default: '' }) imageUrlPrefix!: string
@@ -1094,33 +1094,31 @@ export default class Question extends Vue {
1094
1094
  }
1095
1095
 
1096
1096
  get passageImageUrl () {
1097
- const imageUrl = this.question.images?.passage?.url
1098
- || this.question.images?.Question // Legacy support - TODO: remove when `Question` is deprecated
1097
+ const imageUrl = this.question.passageImage?.url
1099
1098
 
1100
1099
  return imageUrl ? `${this.imageUrlPrefix}${imageUrl}` : null
1101
1100
  }
1102
1101
 
1103
1102
  get passageImageAlt () {
1104
- return this.question.images?.passage?.altText
1103
+ return this.question.passageImage?.altText
1105
1104
  }
1106
1105
 
1107
1106
  get passageImageLongAlt () {
1108
- return this.question.images?.passage?.longAltText
1107
+ return this.question.passageImage?.longAltText
1109
1108
  }
1110
1109
 
1111
1110
  get explanationImageUrl () {
1112
- const imageUrl = this.question.images?.explanation?.url
1113
- || this.question.images?.Explanation // Legacy support - TODO: remove when `Explanation` is deprecated
1111
+ const imageUrl = this.question.explanationImage?.url
1114
1112
 
1115
1113
  return imageUrl ? `${this.imageUrlPrefix}${imageUrl}` : null
1116
1114
  }
1117
1115
 
1118
1116
  get explanationImageAlt () {
1119
- return this.question.images?.explanation?.altText
1117
+ return this.question.explanationImage?.altText
1120
1118
  }
1121
1119
 
1122
1120
  get explanationImageLongAlt () {
1123
- return this.question.images?.explanation?.longAltText
1121
+ return this.question.explanationImage?.longAltText
1124
1122
  }
1125
1123
 
1126
1124
  get showPassageAndImage () {
@@ -1137,18 +1135,15 @@ export default class Question extends Vue {
1137
1135
  }
1138
1136
  }
1139
1137
 
1138
+ get reference () {
1139
+ return this.question.references?.length ? this.question.references.join('') : undefined
1140
+ }
1141
+
1140
1142
  get answers (): TChoice[] {
1141
- const answers = [
1142
- { text: this.question.answer, key: 'a1' },
1143
- { text: this.question.answer2, key: 'a2' },
1144
- { text: this.question.answer3, key: 'a3' },
1145
- { text: this.question.answer4, key: 'a4' },
1146
- { text: this.question.answer5, key: 'a5' },
1147
- { text: this.question.answer6, key: 'a6' },
1148
- { text: this.question.answer7, key: 'a7' },
1149
- { text: this.question.answer8, key: 'a8' },
1150
- { text: this.question.answer9, key: 'a9' },
1151
- ] as const
1143
+ const answers = this.question.choices.filter(choice => choice.isCorrect).map((choice, index) => ({
1144
+ text: choice.text,
1145
+ key: `a${index + 1}` as `a${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`,
1146
+ }))
1152
1147
 
1153
1148
  return answers.filter(choice => !!choice.text)
1154
1149
  }
@@ -1158,17 +1153,10 @@ export default class Question extends Vue {
1158
1153
  }
1159
1154
 
1160
1155
  get distractors (): TChoice[] {
1161
- const distractors = [
1162
- { text: this.question.distractor1, key: 'd1' },
1163
- { text: this.question.distractor2, key: 'd2' },
1164
- { text: this.question.distractor3, key: 'd3' },
1165
- { text: this.question.distractor4, key: 'd4' },
1166
- { text: this.question.distractor5, key: 'd5' },
1167
- { text: this.question.distractor6, key: 'd6' },
1168
- { text: this.question.distractor7, key: 'd7' },
1169
- { text: this.question.distractor8, key: 'd8' },
1170
- { text: this.question.distractor9, key: 'd9' },
1171
- ] as const
1156
+ const distractors = this.question.choices.filter(choice => !choice.isCorrect).map((choice, index) => ({
1157
+ text: choice.text,
1158
+ key: `d${index + 1}` as `d${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`,
1159
+ }))
1172
1160
 
1173
1161
  return distractors.filter(choice => !!choice.text)
1174
1162
  }
@@ -142,6 +142,7 @@ export default class SidePanel extends Vue {
142
142
  notContentHeight = this.tabs && this.tabs.length ? 262 : 218
143
143
  focusListener: Parameters<typeof addEventListener>[1] | null = null
144
144
  savedYPosition = 0
145
+ sidePanelNumber = 0
145
146
 
146
147
  get sidePanelWidth () {
147
148
  return this.width === 'large'
@@ -151,57 +152,67 @@ export default class SidePanel extends Vue {
151
152
  : this.width
152
153
  }
153
154
 
155
+ numberOfSidepanels () {
156
+ return document.querySelectorAll('.uikit-side-panel').length
157
+ }
158
+
154
159
  mounted () {
155
- // Focus title after opening transition
156
- const sidePanelEl = this.$refs['uikit-side-panel'] as HTMLElement | undefined
157
- sidePanelEl?.addEventListener('transitionend', () => {
158
- const titleEl = this.$refs['uikit-side-panel__title'] as HTMLElement | undefined
159
- titleEl?.focus()
160
- })
160
+ this.sidePanelNumber = this.numberOfSidepanels()
161
161
 
162
162
  // delay opening to show animation
163
163
  setTimeout(() => {
164
164
  this.openSidePanel = true
165
165
  }, 1)
166
166
 
167
+
168
+ // Focus title after opening transition
169
+ const sidePanelEl = this.$refs['uikit-side-panel'] as HTMLElement | undefined
170
+ sidePanelEl?.addEventListener('transitionend', () => {
171
+ const titleEl = this.$refs['uikit-side-panel__title'] as HTMLElement | undefined
172
+ titleEl?.focus()
173
+ })
174
+
167
175
  // Trap the user's focus within the side panel - don't allow focusing elements behind the overlay
168
176
  this.focusListener = event => {
169
- const target = (event as FocusEvent).target as HTMLElement // The element receiving focus
170
- const sidePanelContainerEl = this.$refs['sidepanelContainer'] as HTMLElement
171
- const isFocusOutside = target && sidePanelContainerEl && !sidePanelContainerEl.contains(target)
172
- const hasCalendarClass = target
173
- && Array.from(target.classList).find(
174
- c => c === 'button-next-month' || c === 'button-previous-month' || c === 'day-item'
175
- )
176
- if (isFocusOutside && !hasCalendarClass) {
177
- const focusableSidePanelChildren = Array.from<HTMLElement>(sidePanelContainerEl.querySelectorAll(
178
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
179
- ))
180
- const firstFocusableSidePanelChild = focusableSidePanelChildren.find(
181
- el => !!el.getBoundingClientRect().width
182
- )
183
- const reversedSidePanelChildren = [ ...focusableSidePanelChildren ].reverse()
184
- const lastFocusableSidePanelChild = reversedSidePanelChildren.find(
185
- el => !!el.getBoundingClientRect().width
186
- )
187
- if (firstFocusableSidePanelChild) {
188
- const relatedTarget = (event as FocusEvent).relatedTarget // The element last focused
189
- if (relatedTarget === firstFocusableSidePanelChild && lastFocusableSidePanelChild) {
190
- // If focus moves from first element -> outside side panel, focus the last element instead
191
- lastFocusableSidePanelChild.focus()
177
+ if (this.sidePanelNumber === this.numberOfSidepanels()) { // Only focus on the last open panel
178
+ const target = (event as FocusEvent).target as HTMLElement // The element receiving focus
179
+ const sidePanelContainerEl = this.$refs['sidepanelContainer'] as HTMLElement
180
+ const isFocusOutside = target && sidePanelContainerEl && !sidePanelContainerEl.contains(target)
181
+ const hasCalendarClass = target
182
+ && Array.from(target.classList).find(
183
+ c => c === 'button-next-month' || c === 'button-previous-month' || c === 'day-item'
184
+ )
185
+ if (isFocusOutside && !hasCalendarClass) {
186
+ const focusableSidePanelChildren = Array.from<HTMLElement>(sidePanelContainerEl.querySelectorAll(
187
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
188
+ ))
189
+ const firstFocusableSidePanelChild = focusableSidePanelChildren.find(
190
+ el => !!el.getBoundingClientRect().width
191
+ )
192
+ const reversedSidePanelChildren = [ ...focusableSidePanelChildren ].reverse()
193
+ const lastFocusableSidePanelChild = reversedSidePanelChildren.find(
194
+ el => !!el.getBoundingClientRect().width
195
+ )
196
+ if (firstFocusableSidePanelChild) {
197
+ const relatedTarget = (event as FocusEvent).relatedTarget // The element last focused
198
+ if (relatedTarget === firstFocusableSidePanelChild && lastFocusableSidePanelChild) {
199
+ // If focus moves from first element -> outside side panel, focus the last element instead
200
+ lastFocusableSidePanelChild.focus()
201
+ } else {
202
+ // If focus goes outside the side panel in any other way, focus the first element
203
+ firstFocusableSidePanelChild.focus()
204
+ }
192
205
  } else {
193
- // If focus goes outside the side panel in any other way, focus the first element
194
- firstFocusableSidePanelChild.focus()
206
+ // If the side panel doesn't have any focusable children, focus the container instead
207
+ if (sidePanelContainerEl.tabIndex === -1) {
208
+ sidePanelContainerEl.tabIndex = 0
209
+ }
210
+ sidePanelContainerEl.focus()
195
211
  }
196
- } else {
197
- // If the side panel doesn't have any focusable children, focus the container instead
198
- if (sidePanelContainerEl.tabIndex === -1) {
199
- sidePanelContainerEl.tabIndex = 0
200
- }
201
- sidePanelContainerEl.focus()
202
212
  }
203
213
  }
204
214
  }
215
+
205
216
  document.addEventListener('focusin', this.focusListener)
206
217
 
207
218
  // prevent scrolling outside of modal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pocketprep/ui-kit",
3
- "version": "3.0.39",
3
+ "version": "3.1.1",
4
4
  "description": "Pocket Prep UI Kit",
5
5
  "author": "pocketprep",
6
6
  "scripts": {
@@ -77,7 +77,7 @@
77
77
  "vue-facing-decorator": "2.1.20"
78
78
  },
79
79
  "devDependencies": {
80
- "@pocketprep/types": "1.8.10",
80
+ "@pocketprep/types": "1.11.1",
81
81
  "@tsconfig/node16": "1.0.3",
82
82
  "@types/node": "16.18.25",
83
83
  "@vitejs/plugin-vue": "4.2.1",