Skip to content

Commit 85186e9

Browse files
authored
Merge pull request #2665 from pie-framework/develop
merge
2 parents e54e569 + 8f37441 commit 85186e9

8 files changed

Lines changed: 103 additions & 43 deletions

File tree

packages/drag-in-the-blank/controller/src/__tests__/index.test.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,36 +52,36 @@ describe('controller', () => {
5252
describe('correctResponse behavior across modes', () => {
5353
it('does not include correctResponse in gather mode', async () => {
5454
const result = await model(question, {}, { mode: 'gather' });
55-
55+
5656
expect(result.correctResponse).toBeUndefined();
5757
});
58-
58+
5959
it('includes correctResponse in view mode for instructor', async () => {
6060
const result = await model(question, {}, { mode: 'view', role: 'instructor' });
61-
62-
expect(result.correctResponse).toBeDefined();
61+
62+
expect(result.correctResponse).toBeUndefined();
6363
});
64-
64+
6565
it('does not include correctResponse in view mode for student', async () => {
6666
const result = await model(question, {}, { mode: 'view', role: 'student' });
67-
67+
6868
expect(result.correctResponse).toBeUndefined();
6969
});
70-
70+
7171
it('includes correctResponse in evaluate mode', async () => {
7272
const result = await model(question, {}, { mode: 'evaluate' });
73-
73+
7474
expect(result.correctResponse).toBeDefined();
7575
});
76-
76+
7777
it('ensures correctResponse is explicitly undefined when not in evaluate mode or instructor view', async () => {
7878
const gatherResult = await model(question, {}, { mode: 'gather' });
7979
const viewStudentResult = await model(question, {}, { mode: 'view', role: 'student' });
80-
80+
8181
expect(gatherResult.correctResponse).toBeUndefined();
8282
expect(viewStudentResult.correctResponse).toBeUndefined();
8383
});
84-
});
84+
});
8585

8686
const assertGather = (label, extra, session, expected) => {
8787
it(`'mode: gather, ${label}'`, async () => {
@@ -164,7 +164,7 @@ describe('controller', () => {
164164
disabled: true,
165165
feedback: {},
166166
responseCorrect: undefined,
167-
correctResponse: q.correctResponse,
167+
correctResponse: undefined,
168168
...expected,
169169
});
170170
});

packages/drag-in-the-blank/controller/src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function model(question, session, env, updateSession) {
5959
choices = await getShuffledChoices(choices, session, updateSession, 'id');
6060
}
6161

62-
const shouldIncludeCorrectResponse = env.mode === 'evaluate' || (env.role === 'instructor' && env.mode === 'view');
62+
const shouldIncludeCorrectResponse = env.mode === 'evaluate';
6363

6464
const out = {
6565
...normalizedQuestion,
@@ -68,7 +68,7 @@ export function model(question, session, env, updateSession) {
6868
feedback,
6969
mode: env.mode,
7070
disabled: env.mode !== 'gather',
71-
responseCorrect: env.mode === 'evaluate' ? getScore(normalizedQuestion, session) === 1 : undefined,
71+
responseCorrect: shouldIncludeCorrectResponse ? getScore(normalizedQuestion, session) === 1 : undefined,
7272
correctResponse: shouldIncludeCorrectResponse ? normalizedQuestion.correctResponse : undefined,
7373
};
7474

packages/ebsr/docs/demo/generate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ exports.model = (id, element) => ({
22
id,
33
element,
44
partA: {
5-
choiceMode: 'radio',
5+
choiceMode: 'checkbox',
66
choices: [
77
{
88
value: 'yellow',
@@ -24,7 +24,7 @@ exports.model = (id, element) => ({
2424
promptEnabled: true,
2525
},
2626
partB: {
27-
choiceMode: 'radio',
27+
choiceMode: 'checkbox',
2828
choices: [
2929
{
3030
value: 'orange',

packages/image-cloze-association/controller/src/__tests__/index.test.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -546,32 +546,32 @@ describe('controller', () => {
546546
describe('validation property behavior across modes', () => {
547547
it('does not include validation in gather mode', async () => {
548548
const result = await model(question, {}, { mode: 'gather' });
549-
549+
550550
expect(result.validation).toBeUndefined();
551551
});
552-
552+
553553
it('does not include validation in view mode', async () => {
554554
const result = await model(question, {}, { mode: 'view' });
555-
555+
556556
expect(result.validation).toBeUndefined();
557557
});
558-
558+
559559
it('includes validation in evaluate mode and when instructor is in view mode', async () => {
560560
const evalResult = await model(question, {}, { mode: 'evaluate' });
561561
const viewResult = await model(question, {}, { mode: 'view', role: 'instructor' });
562-
562+
563563
expect(evalResult.validation).toBeDefined();
564-
expect(viewResult.validation).toBeDefined();
565-
});
566-
564+
expect(viewResult.validation).toBeUndefined();
565+
});
566+
567567
it('ensures validation is explicitly undefined when not in evaluate or instructor view mode', async () => {
568568
const gatherResult = await model(question, {}, { mode: 'gather' });
569569
const studentViewResult = await model(question, {}, { mode: 'view', role: 'student' });
570-
570+
571571
expect(gatherResult.validation).toBeUndefined();
572572
expect(studentViewResult.validation).toBeUndefined();
573-
});
574-
});
573+
});
574+
});
575575

576576
describe('getPartialScore', () => {
577577
const returnPartialScore = (sess) => {

packages/image-cloze-association/controller/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const model = (question, session, env) => {
1515
const questionCamelized = camelizeKeys(questionNormalized);
1616

1717
return new Promise((resolve) => {
18-
const shouldIncludeCorrectResponse = env.mode === 'evaluate' || (env.role === 'instructor' && env.mode === 'view');
18+
const shouldIncludeCorrectResponse = env.mode === 'evaluate';
1919

2020
const out = {
2121
disabled: env.mode !== 'gather',

packages/multiple-choice/src/choice-input.jsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const inputStyles = {
121121
};
122122

123123
export const StyledCheckbox = withStyles(inputStyles)((props) => {
124-
const { correctness, classes, checked, onChange, disabled, value, id } = props;
124+
const { correctness, classes, checked, onChange, disabled, value, id, onKeyDown, inputRef } = props;
125125
const key = (k) => (correctness ? `${correctness}-${k}` : k);
126126

127127
const resolved = {
@@ -135,7 +135,9 @@ export const StyledCheckbox = withStyles(inputStyles)((props) => {
135135
return (
136136
<Checkbox
137137
id={id}
138+
inputRef={inputRef}
138139
aria-checked={checked}
140+
onKeyDown={onKeyDown}
139141
focusVisibleClassName={checked ? classes.focusVisibleChecked : classes.focusVisibleUnchecked}
140142
disableRipple
141143
{...miniProps}
@@ -150,7 +152,7 @@ export const StyledCheckbox = withStyles(inputStyles)((props) => {
150152
});
151153

152154
export const StyledRadio = withStyles(inputStyles)((props) => {
153-
const { correctness, classes, checked, onChange, disabled, value, id, tagName } = props;
155+
const { correctness, classes, checked, onChange, disabled, value, id, tagName, inputRef } = props;
154156
const key = (k) => (correctness ? `${correctness}-${k}` : k);
155157

156158
const resolved = {
@@ -164,6 +166,7 @@ export const StyledRadio = withStyles(inputStyles)((props) => {
164166
return (
165167
<Radio
166168
id={id}
169+
inputRef={inputRef}
167170
aria-checked={checked}
168171
focusVisibleClassName={checked ? classes.focusVisibleChecked : classes.focusVisibleUnchecked}
169172
disableRipple
@@ -193,6 +196,7 @@ export class ChoiceInput extends React.Component {
193196
value: PropTypes.string.isRequired,
194197
classes: PropTypes.object,
195198
className: PropTypes.string,
199+
tagName: PropTypes.string,
196200
hideTick: PropTypes.bool,
197201
isEvaluateMode: PropTypes.bool,
198202
choicesLayout: PropTypes.oneOf(['vertical', 'grid', 'horizontal']),
@@ -220,6 +224,37 @@ export class ChoiceInput extends React.Component {
220224
return 'choice-' + (Math.random() * 10000).toFixed();
221225
}
222226

227+
handleKeyDown = (event) => {
228+
const { choiceMode } = this.props;
229+
230+
if (choiceMode !== 'checkbox') return;
231+
232+
const isArrowDown = event.key === 'ArrowDown';
233+
const isArrowUp = event.key === 'ArrowUp';
234+
235+
if (!isArrowDown && !isArrowUp) return;
236+
237+
event.preventDefault();
238+
239+
const currentEl = document.getElementById(this.choiceId);
240+
if (!currentEl) return;
241+
242+
const fieldset = currentEl.closest('fieldset');
243+
if (!fieldset) return;
244+
245+
const groupCheckboxes = Array.from(fieldset.querySelectorAll('input[type="checkbox"]'));
246+
247+
const currentIndex = groupCheckboxes.findIndex((el) => el === currentEl);
248+
if (currentIndex === -1) return;
249+
250+
const nextIndex = isArrowDown ? currentIndex + 1 : currentIndex - 1;
251+
const nextEl = groupCheckboxes[nextIndex];
252+
253+
if (nextEl) {
254+
nextEl.focus();
255+
}
256+
};
257+
223258
render() {
224259
const {
225260
choiceMode,
@@ -276,6 +311,7 @@ export class ChoiceInput extends React.Component {
276311
value,
277312
id: this.choiceId,
278313
onChange: this.onToggleChoice,
314+
onKeyDown: this.handleKeyDown,
279315
'aria-describedby': this.descId,
280316
};
281317

@@ -298,7 +334,7 @@ export class ChoiceInput extends React.Component {
298334
) : (
299335
<>
300336
{hasMathOrImage && screenReaderLabel}
301-
<Tag {...tagProps} />
337+
<Tag {...tagProps} inputRef={this.props.autoFocusRef} />
302338
</>
303339
);
304340

packages/multiple-choice/src/choice.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class Choice extends React.Component {
3333
gridColumns,
3434
isSelectionButtonBelow,
3535
selectedAnswerBackgroundColor,
36+
autoFocusRef,
3637
tagName
3738
} = this.props;
3839
const choiceClass = 'choice' + (index === choicesLength - 1 ? ' last' : '');
@@ -65,7 +66,7 @@ export class Choice extends React.Component {
6566

6667
return (
6768
<div className={choiceClass} key={index} style={{ backgroundColor: choiceBackground }}>
68-
<ChoiceInput {...choiceProps} className={names} />
69+
<ChoiceInput {...choiceProps} className={names} autoFocusRef={autoFocusRef} />
6970
</div>
7071
);
7172
}
@@ -88,7 +89,8 @@ Choice.propTypes = {
8889
gridColumns: PropTypes.string,
8990
selectedAnswerBackgroundColor: PropTypes.string,
9091
tagName: PropTypes.string,
91-
isSelectionButtonBelow: PropTypes.bool
92+
isSelectionButtonBelow: PropTypes.bool,
93+
autoFocusRef: PropTypes.object,
9294
};
9395

9496
export default withStyles((theme) => ({

packages/multiple-choice/src/multiple-choice.jsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class MultipleChoice extends React.Component {
9494
customAudioButton: {
9595
playImage: PropTypes.string,
9696
pauseImage: PropTypes.string,
97-
}
97+
},
9898
};
9999

100100
constructor(props) {
@@ -106,6 +106,7 @@ export class MultipleChoice extends React.Component {
106106
};
107107

108108
this.onToggle = this.onToggle.bind(this);
109+
this.firstInputRef = React.createRef();
109110
}
110111

111112
isSelected(value) {
@@ -222,6 +223,19 @@ export class MultipleChoice extends React.Component {
222223
);
223224
}
224225

226+
handleGroupFocus = (e) => {
227+
const fieldset = e.currentTarget;
228+
const activeEl = document.activeElement;
229+
230+
if (fieldset.contains(activeEl) && activeEl !== fieldset) {
231+
return;
232+
}
233+
234+
if (this.firstInputRef?.current) {
235+
this.firstInputRef.current.focus();
236+
}
237+
};
238+
225239
render() {
226240
const {
227241
mode,
@@ -243,7 +257,7 @@ export class MultipleChoice extends React.Component {
243257
maxSelections,
244258
autoplayAudioEnabled,
245259
session,
246-
customAudioButton
260+
customAudioButton,
247261
} = this.props;
248262
const { showCorrect, maxSelectionsErrorState } = this.state;
249263
const isEvaluateMode = mode === 'evaluate';
@@ -264,7 +278,11 @@ export class MultipleChoice extends React.Component {
264278
if (minSelections && maxSelections) {
265279
return minSelections === maxSelections
266280
? translator.t('translation:multipleChoice:minmaxSelections_equal', { lng: language, minSelections })
267-
: translator.t('translation:multipleChoice:minmaxSelections_range', { lng: language, minSelections, maxSelections });
281+
: translator.t('translation:multipleChoice:minmaxSelections_range', {
282+
lng: language,
283+
minSelections,
284+
maxSelections,
285+
});
268286
}
269287

270288
if (minSelections) {
@@ -275,7 +293,7 @@ export class MultipleChoice extends React.Component {
275293
};
276294

277295
return (
278-
<div id={'main-container'} className={classNames(classes.main, className, 'multiple-choice')}>
296+
<div id={'main-container'} className={classNames(classes.main, className, 'multiple-choice')}>
279297
{partLabel && <h3 className={classes.partLabel}>{partLabel}</h3>}
280298

281299
{this.renderHeading()}
@@ -297,7 +315,12 @@ export class MultipleChoice extends React.Component {
297315
</div>
298316
)}
299317

300-
<fieldset tabIndex={0} className={classes.fieldset} role={choiceMode === 'radio' ? 'radiogroup' : 'group'}>
318+
<fieldset
319+
tabIndex={0}
320+
className={classes.fieldset}
321+
onFocus={this.handleGroupFocus}
322+
role={choiceMode === 'radio' ? 'radiogroup' : 'group'}
323+
>
301324
<PreviewPrompt
302325
className="prompt"
303326
defaultClassName="prompt"
@@ -325,6 +348,7 @@ export class MultipleChoice extends React.Component {
325348
>
326349
{choices.map((choice, index) => (
327350
<StyledChoice
351+
autoFocusRef={index === 0 ? this.firstInputRef : null}
328352
choicesLayout={this.props.choicesLayout}
329353
selectedAnswerBackgroundColor={this.props.selectedAnswerBackgroundColor}
330354
gridColumns={gridColumns}
@@ -336,7 +360,7 @@ export class MultipleChoice extends React.Component {
336360
isEvaluateMode={isEvaluateMode}
337361
choiceMode={choiceMode}
338362
disabled={disabled}
339-
tagName={partLabel ? `group-${partLabel}`: 'group'}
363+
tagName={partLabel ? `group-${partLabel}` : 'group'}
340364
onChoiceChanged={this.handleChange}
341365
hideTick={choice.hideTick}
342366
checked={this.getChecked(choice)}
@@ -348,10 +372,8 @@ export class MultipleChoice extends React.Component {
348372
</div>
349373
</fieldset>
350374

351-
{choiceMode === 'checkbox' && (selections < minSelections) && (
352-
<div className={classes.errorText}>
353-
{getMultipleChoiceMinSelectionErrorMessage()}
354-
</div>
375+
{choiceMode === 'checkbox' && selections < minSelections && (
376+
<div className={classes.errorText}>{getMultipleChoiceMinSelectionErrorMessage()}</div>
355377
)}
356378
{choiceMode === 'checkbox' && maxSelectionsErrorState && (
357379
<div className={classes.errorText}>

0 commit comments

Comments
 (0)