@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.
- package/dist/@pocketprep/ui-kit.js +7140 -7144
- package/dist/@pocketprep/ui-kit.js.map +1 -1
- package/dist/@pocketprep/ui-kit.umd.cjs +9 -9
- package/dist/@pocketprep/ui-kit.umd.cjs.map +1 -1
- package/lib/components/Modal/ModalContainer.vue +24 -18
- package/lib/components/Quiz/Question.vue +28 -40
- package/lib/components/SidePanels/SidePanel.vue +48 -37
- package/package.json +2 -2
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]'
|
|
65
|
+
|
|
66
|
+
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]'
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
// Otherwise, just return focus to the first element
|
|
103
109
|
firstFocusableModalChild.focus()
|
|
104
110
|
}
|
|
105
111
|
} else {
|
|
106
|
-
|
|
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.
|
|
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
|
-
!
|
|
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="
|
|
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="
|
|
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
|
-
!
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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.
|
|
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.
|
|
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.
|
|
1103
|
+
return this.question.passageImage?.altText
|
|
1105
1104
|
}
|
|
1106
1105
|
|
|
1107
1106
|
get passageImageLongAlt () {
|
|
1108
|
-
return this.question.
|
|
1107
|
+
return this.question.passageImage?.longAltText
|
|
1109
1108
|
}
|
|
1110
1109
|
|
|
1111
1110
|
get explanationImageUrl () {
|
|
1112
|
-
const imageUrl = this.question.
|
|
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.
|
|
1117
|
+
return this.question.explanationImage?.altText
|
|
1120
1118
|
}
|
|
1121
1119
|
|
|
1122
1120
|
get explanationImageLongAlt () {
|
|
1123
|
-
return this.question.
|
|
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
|
-
|
|
1143
|
-
{
|
|
1144
|
-
|
|
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
|
-
|
|
1163
|
-
{
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
194
|
-
|
|
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.
|
|
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.
|
|
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",
|