@khanacademy/perseus 17.6.0 → 17.6.2
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/CHANGELOG.md +17 -0
- package/dist/es/index.js +402 -606
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +411 -603
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +2 -1
- package/dist/strings.js +1 -0
- package/dist/widgets/deprecated-standin/index.d.ts +32 -0
- package/dist/widgets/label-image/answer-choices.d.ts +8 -40
- package/dist/widgets/label-image/answer-pill.d.ts +3 -1
- package/dist/widgets/label-image/marker.d.ts +10 -5
- package/dist/widgets/label-image.d.ts +1 -8
- package/dist/widgets.d.ts +9 -0
- package/package.json +34 -33
- package/src/__tests__/widgets.test.ts +20 -0
- package/src/extra-widgets.ts +2 -0
- package/src/init.ts +5 -0
- package/src/perseus-types.ts +4 -1
- package/src/util/register-all-widgets-for-testing.ts +4 -0
- package/src/widgets/__stories__/orderer.stories.tsx +5 -1
- package/src/widgets/__testdata__/orderer.testdata.ts +66 -0
- package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +34 -0
- package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +71 -0
- package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +93 -0
- package/src/widgets/deprecated-standin/index.tsx +57 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +29 -34
- package/src/widgets/label-image/__tests__/answer-pill.test.tsx +2 -10
- package/src/widgets/label-image/answer-choices.tsx +60 -278
- package/src/widgets/label-image/answer-pill.tsx +22 -13
- package/src/widgets/label-image/marker.tsx +103 -146
- package/src/widgets/label-image.test.ts +0 -2
- package/src/widgets/label-image.tsx +94 -195
- package/src/widgets.ts +24 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import "@testing-library/jest-dom"; // Imports custom matchers
|
|
2
|
+
|
|
3
|
+
import {testDependencies} from "../../../../../../testing/test-dependencies";
|
|
4
|
+
import * as Dependencies from "../../../dependencies";
|
|
5
|
+
import {renderQuestion} from "../../__tests__/renderQuestion";
|
|
6
|
+
|
|
7
|
+
const question = {
|
|
8
|
+
content:
|
|
9
|
+
"Read the excerpt and answer the question below. \n\nThe Governor and Council of the Massachusetts had much conference many days; and at last . . . . concluded a peace and friendship with [[\u2603 widget 1]], upon these conditions.",
|
|
10
|
+
images: {},
|
|
11
|
+
widgets: {
|
|
12
|
+
"widget 1": {
|
|
13
|
+
graded: true,
|
|
14
|
+
version: {
|
|
15
|
+
major: 0,
|
|
16
|
+
minor: 0,
|
|
17
|
+
},
|
|
18
|
+
static: false,
|
|
19
|
+
type: "deprecated-standin",
|
|
20
|
+
options: {
|
|
21
|
+
static: false,
|
|
22
|
+
},
|
|
23
|
+
alignment: "default",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
describe("Deprecated Standin widget", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.useRealTimers();
|
|
31
|
+
|
|
32
|
+
jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
|
|
33
|
+
testDependencies,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should have a default snapshot", () => {
|
|
38
|
+
// Arrange & Act
|
|
39
|
+
const {container} = renderQuestion(question);
|
|
40
|
+
|
|
41
|
+
// Assert
|
|
42
|
+
expect(container).toMatchSnapshot("first render");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should be scorable and always give points", () => {
|
|
46
|
+
// Arrange and act
|
|
47
|
+
const {renderer} = renderQuestion(question);
|
|
48
|
+
const result = renderer.scoreWidgets();
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(result["widget 1"]).toMatchObject({
|
|
52
|
+
type: "points",
|
|
53
|
+
earned: 1,
|
|
54
|
+
total: 1,
|
|
55
|
+
message: null,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return an empty object for getUserInput()", () => {
|
|
60
|
+
// Arrange
|
|
61
|
+
const {renderer} = renderQuestion(question);
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
const userInput = renderer.getUserInput();
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
expect(userInput).toMatchInlineSnapshot(`
|
|
68
|
+
[
|
|
69
|
+
{},
|
|
70
|
+
]
|
|
71
|
+
`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return a correct answer score for simpleValidate()", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const {renderer} = renderQuestion(question);
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const score = renderer.scoreWidgets();
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(score).toMatchInlineSnapshot(`
|
|
83
|
+
{
|
|
84
|
+
"widget 1": {
|
|
85
|
+
"earned": 1,
|
|
86
|
+
"message": null,
|
|
87
|
+
"total": 1,
|
|
88
|
+
"type": "points",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Banner from "@khanacademy/wonder-blocks-banner";
|
|
2
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import type {PerseusScore, WidgetExports} from "../../types";
|
|
6
|
+
|
|
7
|
+
// The props are type `any` on purpose so that this can receive props
|
|
8
|
+
// from any deprecated widget
|
|
9
|
+
type AutoCorrectProps = any;
|
|
10
|
+
type Rubric = any;
|
|
11
|
+
type UserInput = Empty;
|
|
12
|
+
|
|
13
|
+
class AutoCorrect extends React.Component<AutoCorrectProps> {
|
|
14
|
+
static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
|
|
15
|
+
// Since this mean to replace an existing widget the learner
|
|
16
|
+
// WILL earn points for this widget
|
|
17
|
+
return {
|
|
18
|
+
type: "points",
|
|
19
|
+
earned: 1,
|
|
20
|
+
total: 1,
|
|
21
|
+
message: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getUserInput: () => UserInput = () => {
|
|
26
|
+
return {};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
simpleValidate: (arg1: Rubric) => PerseusScore = (rubric) => {
|
|
30
|
+
return AutoCorrect.validate(this.getUserInput(), rubric);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
paddingTop: 8,
|
|
38
|
+
paddingBottom: 8,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<Banner
|
|
42
|
+
text={i18n._(
|
|
43
|
+
"This part of the question is no-longer available. You will not be graded on this part.",
|
|
44
|
+
)}
|
|
45
|
+
kind="warning"
|
|
46
|
+
layout="full-width"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
name: "deprecated-standin",
|
|
55
|
+
displayName: "Deprecated Standin",
|
|
56
|
+
widget: AutoCorrect,
|
|
57
|
+
} as WidgetExports<typeof AutoCorrect>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
|
|
1
2
|
import * as React from "react";
|
|
2
3
|
|
|
3
4
|
import AnswerChoices from "../answer-choices";
|
|
4
5
|
|
|
5
|
-
import type {AnswerType} from "../answer-choices";
|
|
6
|
-
|
|
7
6
|
type StoryArgs = Record<any, any>;
|
|
8
7
|
|
|
9
8
|
type Story = {
|
|
@@ -45,47 +44,43 @@ const defaultChoices = [
|
|
|
45
44
|
},
|
|
46
45
|
];
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
choices: ReadonlyArray<AnswerType>;
|
|
54
|
-
}
|
|
55
|
-
> {
|
|
56
|
-
static defaultProps = {
|
|
57
|
-
multipleSelect: false,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
state = {
|
|
61
|
-
choices: [...defaultChoices],
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
handleChange(selection) {
|
|
65
|
-
const {choices} = this.state;
|
|
47
|
+
const WithState = ({multipleSelect}) => {
|
|
48
|
+
const [choices, setChoices] = React.useState([...defaultChoices]);
|
|
49
|
+
const [isOpened, setIsOpened] = React.useState(false);
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
|
|
51
|
+
const handleChange = (selection) => {
|
|
52
|
+
setChoices([
|
|
53
|
+
...choices.map((choice, index) => ({
|
|
69
54
|
...choice,
|
|
70
55
|
checked: selection[index],
|
|
71
56
|
})),
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
render(): React.ReactNode {
|
|
76
|
-
const {multipleSelect} = this.props;
|
|
77
|
-
|
|
78
|
-
const {choices} = this.state;
|
|
57
|
+
]);
|
|
58
|
+
};
|
|
79
59
|
|
|
80
|
-
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
81
62
|
<AnswerChoices
|
|
82
63
|
choices={choices}
|
|
83
64
|
multipleSelect={multipleSelect}
|
|
84
|
-
onChange={(selection) =>
|
|
65
|
+
onChange={(selection) => handleChange(selection)}
|
|
66
|
+
opener={() => <button>{isOpened ? "Close" : "Open"}</button>}
|
|
67
|
+
onToggle={(opened) => setIsOpened(opened)}
|
|
68
|
+
disabled={false}
|
|
85
69
|
/>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
70
|
+
<>
|
|
71
|
+
{choices
|
|
72
|
+
.filter(({checked}) => checked)
|
|
73
|
+
.map(({content}) => (
|
|
74
|
+
<LabelLarge key={content}>{content}</LabelLarge>
|
|
75
|
+
))}
|
|
76
|
+
</>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
WithState.defaultProps = {
|
|
82
|
+
multipleSelect: false,
|
|
83
|
+
};
|
|
89
84
|
|
|
90
85
|
export const SingleSelect = (args: StoryArgs): React.ReactElement => {
|
|
91
86
|
return <WithState />;
|
|
@@ -17,11 +17,7 @@ describe("AnswerPill", () => {
|
|
|
17
17
|
it("should display the correct answer string when there is only one selected answer", () => {
|
|
18
18
|
const selectedAnswers = ["Answer 1"];
|
|
19
19
|
const screen = render(
|
|
20
|
-
<AnswerPill
|
|
21
|
-
selectedAnswers={selectedAnswers}
|
|
22
|
-
side={"top"}
|
|
23
|
-
onClick={() => {}}
|
|
24
|
-
/>,
|
|
20
|
+
<AnswerPill selectedAnswers={selectedAnswers} side={"top"} />,
|
|
25
21
|
{wrapper: RenderStateRoot},
|
|
26
22
|
);
|
|
27
23
|
expect(screen.getByText("Answer 1")).toBeInTheDocument();
|
|
@@ -30,11 +26,7 @@ describe("AnswerPill", () => {
|
|
|
30
26
|
it("should display the correct answer string when there are multiple selected answers", () => {
|
|
31
27
|
const selectedAnswers = ["Answer 1", "Answer 2", "Answer 3"];
|
|
32
28
|
const screen = render(
|
|
33
|
-
<AnswerPill
|
|
34
|
-
selectedAnswers={selectedAnswers}
|
|
35
|
-
side={"top"}
|
|
36
|
-
onClick={() => {}}
|
|
37
|
-
/>,
|
|
29
|
+
<AnswerPill selectedAnswers={selectedAnswers} side={"top"} />,
|
|
38
30
|
{wrapper: RenderStateRoot},
|
|
39
31
|
);
|
|
40
32
|
expect(screen.getByText("3 answers")).toBeInTheDocument();
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Enables single or multiple answers selection, using radio or checkbox input
|
|
5
|
-
* types, respectively.
|
|
2
|
+
* Enables single or multiple answers selection using WonderBlocks dropdowns.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import
|
|
9
|
-
|
|
5
|
+
import {
|
|
6
|
+
MultiSelect,
|
|
7
|
+
SingleSelect,
|
|
8
|
+
OptionItem,
|
|
9
|
+
} from "@khanacademy/wonder-blocks-dropdown";
|
|
10
10
|
import * as React from "react";
|
|
11
11
|
import _ from "underscore";
|
|
12
12
|
|
|
13
|
-
import Icon from "../../components/icon";
|
|
14
|
-
import {iconCheckMedium} from "../../icon-paths";
|
|
15
13
|
import Renderer from "../../renderer";
|
|
16
|
-
import sharedStyles from "../../styles/shared";
|
|
17
14
|
|
|
18
15
|
export type AnswerType = {
|
|
19
16
|
// The answer string, can be plain text or a KaTeX expression.
|
|
@@ -22,25 +19,6 @@ export type AnswerType = {
|
|
|
22
19
|
checked: boolean;
|
|
23
20
|
};
|
|
24
21
|
|
|
25
|
-
type AnswerProps = AnswerType & {
|
|
26
|
-
// The input control type to use.
|
|
27
|
-
inputType: "checkbox" | "radio";
|
|
28
|
-
// The radio group name.
|
|
29
|
-
groupName: string;
|
|
30
|
-
// The answer position within choices.
|
|
31
|
-
index: number;
|
|
32
|
-
// Callback to handle change to answer selection.
|
|
33
|
-
onChange: (checked: boolean) => void;
|
|
34
|
-
// Callbacks to handle navigating between adjacent answers.
|
|
35
|
-
onFocusPrevAnswer: () => void;
|
|
36
|
-
onFocusNextAnswer: () => void;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type AnswerState = {
|
|
40
|
-
// Whether answer is currently focused.
|
|
41
|
-
isInputFocused: boolean;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
22
|
type AnswerChoicesProps = {
|
|
45
23
|
// The list of possible answers in a specific order.
|
|
46
24
|
choices: ReadonlyArray<AnswerType>;
|
|
@@ -48,259 +26,63 @@ type AnswerChoicesProps = {
|
|
|
48
26
|
multipleSelect: boolean;
|
|
49
27
|
// Callback to handle change to answer choices selection.
|
|
50
28
|
onChange: (selection: ReadonlyArray<boolean>) => void;
|
|
29
|
+
// Callback to define custom opener.
|
|
30
|
+
opener: SingleSelect["props"]["opener"];
|
|
31
|
+
// Callback to handle toggle of dropdown.
|
|
32
|
+
onToggle: (opened: boolean) => unknown;
|
|
33
|
+
// Whether the answer choices are disabled.
|
|
34
|
+
disabled: boolean;
|
|
51
35
|
};
|
|
52
36
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class AnswerChoice extends React.Component<AnswerProps, AnswerState> {
|
|
59
|
-
// The radio button or checkbox input control.
|
|
60
|
-
_input: HTMLInputElement | null | undefined;
|
|
61
|
-
|
|
62
|
-
state: AnswerState = {
|
|
63
|
-
isInputFocused: false,
|
|
37
|
+
const AnswerChoices = (props: AnswerChoicesProps) => {
|
|
38
|
+
const onAnswerChange = (selected: string[]) => {
|
|
39
|
+
const {choices, onChange} = props;
|
|
40
|
+
onChange(choices.map((choice) => selected.includes(choice.content)));
|
|
64
41
|
};
|
|
65
42
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
break;
|
|
95
|
-
|
|
96
|
-
case "ArrowDown":
|
|
97
|
-
e.preventDefault();
|
|
98
|
-
onFocusNextAnswer();
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
handleInputChange(e: React.SyntheticEvent<HTMLInputElement>) {
|
|
104
|
-
// Prevent answer selection from double-toggling, as `handleClick` is
|
|
105
|
-
// handling changes to selection state.
|
|
106
|
-
e.preventDefault();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
handleInputFocus() {
|
|
110
|
-
this.setState({isInputFocused: true});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
handleInputBlur() {
|
|
114
|
-
this.setState({isInputFocused: false});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
renderCheckmark(): React.ReactNode {
|
|
118
|
-
const {checked} = this.props;
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<span className={css(styles.checkmarkContainer)}>
|
|
122
|
-
{checked && (
|
|
123
|
-
<Icon
|
|
124
|
-
icon={iconCheckMedium}
|
|
125
|
-
size={16}
|
|
126
|
-
color={Color.offBlack}
|
|
127
|
-
/>
|
|
128
|
-
)}
|
|
129
|
-
</span>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
render(): React.ReactNode {
|
|
134
|
-
const {content, checked, groupName, index, inputType} = this.props;
|
|
135
|
-
|
|
136
|
-
const {isInputFocused} = this.state;
|
|
137
|
-
|
|
138
|
-
const answerId = `${groupName}-${index}`;
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<div
|
|
142
|
-
className={css(
|
|
143
|
-
styles.answerChoice,
|
|
144
|
-
isInputFocused && styles.answerFocused,
|
|
145
|
-
)}
|
|
146
|
-
onClick={(e) => this.handleClick(e)}
|
|
147
|
-
onKeyDown={(e) => this.handleKeyDown(e)}
|
|
148
|
-
>
|
|
149
|
-
<span className={css(styles.answerInputWrapper)}>
|
|
150
|
-
<input
|
|
151
|
-
className={css(sharedStyles.perseusSrOnly)}
|
|
152
|
-
checked={checked}
|
|
153
|
-
name={groupName}
|
|
154
|
-
id={answerId}
|
|
155
|
-
type={inputType}
|
|
156
|
-
onChange={(e) => this.handleInputChange(e)}
|
|
157
|
-
onFocus={() => this.handleInputFocus()}
|
|
158
|
-
onBlur={() => this.handleInputBlur()}
|
|
159
|
-
ref={(node) => (this._input = node)}
|
|
160
|
-
/>
|
|
161
|
-
|
|
162
|
-
{this.renderCheckmark()}
|
|
163
|
-
</span>
|
|
164
|
-
|
|
165
|
-
<label htmlFor={answerId}>
|
|
166
|
-
<Renderer content={content} />
|
|
167
|
-
</label>
|
|
168
|
-
</div>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export default class AnswerChoices extends React.Component<
|
|
174
|
-
AnswerChoicesProps,
|
|
175
|
-
AnswerChoicesState
|
|
176
|
-
> {
|
|
177
|
-
// The rendered answers elements.
|
|
178
|
-
_choices: Array<AnswerChoice | null | undefined>;
|
|
179
|
-
|
|
180
|
-
constructor(props: AnswerChoicesProps) {
|
|
181
|
-
super(props);
|
|
182
|
-
|
|
183
|
-
this._choices = [];
|
|
184
|
-
|
|
185
|
-
this.state = {
|
|
186
|
-
groupName: _.uniqueId("answer_choices_"),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
focusAnswer(index = 0) {
|
|
191
|
-
const numChoices = this.props.choices.length;
|
|
192
|
-
|
|
193
|
-
// Wrap the index if necessary.
|
|
194
|
-
if (index < 0) {
|
|
195
|
-
index += numChoices;
|
|
196
|
-
} else if (index >= numChoices) {
|
|
197
|
-
index -= numChoices;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const choice = this._choices[index];
|
|
201
|
-
|
|
202
|
-
if (choice) {
|
|
203
|
-
choice.focusInput();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
onAnswerChange(index: number, checked: boolean) {
|
|
208
|
-
const {choices, multipleSelect, onChange} = this.props;
|
|
209
|
-
|
|
210
|
-
const selection: Array<boolean> = [];
|
|
211
|
-
|
|
212
|
-
// Compile the selection state of answer choices. In single selection
|
|
213
|
-
// mode (radio input type), only 0 or 1 answer may be chosen.
|
|
214
|
-
for (let i = 0; i < choices.length; i++) {
|
|
215
|
-
selection.push(
|
|
216
|
-
multipleSelect
|
|
217
|
-
? i === index
|
|
218
|
-
? checked
|
|
219
|
-
: choices[i].checked
|
|
220
|
-
: i === index
|
|
221
|
-
? checked
|
|
222
|
-
: false,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
onChange(selection);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
onFocusAnswer(index: number) {
|
|
230
|
-
this.focusAnswer(index);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
render(): React.ReactNode {
|
|
234
|
-
const {choices, multipleSelect} = this.props;
|
|
235
|
-
|
|
236
|
-
const {groupName} = this.state;
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<fieldset
|
|
240
|
-
style={{
|
|
241
|
-
border: "none",
|
|
242
|
-
margin: 0,
|
|
243
|
-
padding: 0,
|
|
244
|
-
}}
|
|
245
|
-
>
|
|
246
|
-
{choices.map((choice, index) => (
|
|
247
|
-
<AnswerChoice
|
|
248
|
-
{...choice}
|
|
249
|
-
key={choice.content}
|
|
250
|
-
groupName={groupName}
|
|
251
|
-
index={index}
|
|
252
|
-
inputType={multipleSelect ? "checkbox" : "radio"}
|
|
253
|
-
onChange={(checked) =>
|
|
254
|
-
this.onAnswerChange(index, checked)
|
|
255
|
-
}
|
|
256
|
-
onFocusPrevAnswer={() => this.onFocusAnswer(index - 1)}
|
|
257
|
-
onFocusNextAnswer={() => this.onFocusAnswer(index + 1)}
|
|
258
|
-
ref={(node) => (this._choices[index] = node)}
|
|
259
|
-
/>
|
|
260
|
-
))}
|
|
261
|
-
</fieldset>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const styles = StyleSheet.create({
|
|
267
|
-
answerChoice: {
|
|
268
|
-
display: "flex",
|
|
269
|
-
alignItems: "center",
|
|
270
|
-
|
|
271
|
-
userSelect: "none",
|
|
272
|
-
|
|
273
|
-
padding: "10px 16px 10px 0px",
|
|
274
|
-
|
|
275
|
-
fontWeight: "bold",
|
|
276
|
-
|
|
277
|
-
border: "solid 2px transparent",
|
|
278
|
-
borderRadius: 2,
|
|
279
|
-
|
|
280
|
-
":active": {
|
|
281
|
-
backgroundColor: "rgba(33, 36, 44, 0.08)",
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
|
|
285
|
-
answerFocused: {
|
|
286
|
-
borderColor: "#1865f2",
|
|
287
|
-
|
|
288
|
-
":active": {
|
|
289
|
-
borderColor: "transparent",
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
|
|
293
|
-
answerInputWrapper: {
|
|
294
|
-
marginRight: 8,
|
|
295
|
-
},
|
|
296
|
-
|
|
297
|
-
checkmarkContainer: {
|
|
298
|
-
display: "flex",
|
|
299
|
-
position: "relative",
|
|
43
|
+
// WB Dropdown types only take an array of nodes, so we can't use a
|
|
44
|
+
// functional component, which can only return a single node.
|
|
45
|
+
const AnswerItems = (choices: readonly AnswerType[]) =>
|
|
46
|
+
choices.map(({content}) => (
|
|
47
|
+
<OptionItem
|
|
48
|
+
key={content}
|
|
49
|
+
value={content}
|
|
50
|
+
label={<Renderer content={content} inline />}
|
|
51
|
+
/>
|
|
52
|
+
));
|
|
53
|
+
|
|
54
|
+
const selectedValues = props.choices
|
|
55
|
+
.filter((choice) => choice.checked)
|
|
56
|
+
.map((choice) => choice.content);
|
|
57
|
+
|
|
58
|
+
const {opener, onToggle, disabled} = props;
|
|
59
|
+
|
|
60
|
+
const args: Partial<
|
|
61
|
+
React.ComponentProps<typeof MultiSelect> &
|
|
62
|
+
React.ComponentProps<typeof SingleSelect>
|
|
63
|
+
> = {
|
|
64
|
+
// reset to allow child (answer pill) to control z-index
|
|
65
|
+
style: {zIndex: "unset"},
|
|
66
|
+
children: AnswerItems(props.choices),
|
|
67
|
+
opener,
|
|
68
|
+
onToggle,
|
|
69
|
+
disabled,
|
|
70
|
+
};
|
|
300
71
|
|
|
301
|
-
|
|
302
|
-
|
|
72
|
+
return props.multipleSelect ? (
|
|
73
|
+
<MultiSelect
|
|
74
|
+
{...args}
|
|
75
|
+
selectedValues={selectedValues}
|
|
76
|
+
onChange={(selected) => onAnswerChange(selected)}
|
|
77
|
+
/>
|
|
78
|
+
) : (
|
|
79
|
+
<SingleSelect
|
|
80
|
+
{...args}
|
|
81
|
+
selectedValue={selectedValues[0]}
|
|
82
|
+
onChange={(selected) => onAnswerChange([selected])}
|
|
83
|
+
placeholder={props.choices[0].content} // not visible, but required
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
303
87
|
|
|
304
|
-
|
|
305
|
-
},
|
|
306
|
-
});
|
|
88
|
+
export default AnswerChoices;
|
|
@@ -8,16 +8,31 @@ import {Popper} from "react-popper";
|
|
|
8
8
|
|
|
9
9
|
import Renderer from "../../renderer";
|
|
10
10
|
|
|
11
|
+
const BringToFront: CSSProperties = {
|
|
12
|
+
boxShadow: `0 8px 8px ${Color.offBlack64}`,
|
|
13
|
+
zIndex: 1000,
|
|
14
|
+
};
|
|
15
|
+
|
|
11
16
|
export const AnswerPill = (props: {
|
|
12
17
|
selectedAnswers: readonly string[];
|
|
13
18
|
showCorrectness?: "correct" | "incorrect";
|
|
14
19
|
markerRef?: HTMLElement;
|
|
15
20
|
side: "top" | "bottom" | "left" | "right";
|
|
16
|
-
onClick: () => void;
|
|
17
21
|
style?: CSSProperties;
|
|
22
|
+
hovered?: boolean;
|
|
23
|
+
focused?: boolean;
|
|
24
|
+
onClick?: () => void;
|
|
18
25
|
}) => {
|
|
19
|
-
const {
|
|
20
|
-
|
|
26
|
+
const {
|
|
27
|
+
selectedAnswers,
|
|
28
|
+
showCorrectness,
|
|
29
|
+
markerRef,
|
|
30
|
+
side,
|
|
31
|
+
onClick,
|
|
32
|
+
style,
|
|
33
|
+
focused,
|
|
34
|
+
hovered,
|
|
35
|
+
} = props;
|
|
21
36
|
|
|
22
37
|
const idFactory = useUniqueIdWithMock();
|
|
23
38
|
|
|
@@ -36,11 +51,6 @@ export const AnswerPill = (props: {
|
|
|
36
51
|
const correct = showCorrectness === "correct";
|
|
37
52
|
const incorrect = showCorrectness === "incorrect";
|
|
38
53
|
|
|
39
|
-
const bringToFront = {
|
|
40
|
-
boxShadow: `0 8px 8px ${Color.offBlack64}`,
|
|
41
|
-
zIndex: 1000,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
54
|
return (
|
|
45
55
|
<Popper
|
|
46
56
|
placement={side}
|
|
@@ -64,12 +74,10 @@ export const AnswerPill = (props: {
|
|
|
64
74
|
style={[
|
|
65
75
|
style,
|
|
66
76
|
popperStyle,
|
|
67
|
-
|
|
68
|
-
":hover": bringToFront,
|
|
69
|
-
":focus": bringToFront,
|
|
70
|
-
},
|
|
77
|
+
styles.relative,
|
|
71
78
|
correct && styles.correct,
|
|
72
79
|
incorrect && styles.incorrect,
|
|
80
|
+
(focused || hovered) && BringToFront,
|
|
73
81
|
]}
|
|
74
82
|
>
|
|
75
83
|
<Renderer content={answerString} inline />
|
|
@@ -81,7 +89,8 @@ export const AnswerPill = (props: {
|
|
|
81
89
|
|
|
82
90
|
const styles = StyleSheet.create({
|
|
83
91
|
correct: {
|
|
84
|
-
|
|
92
|
+
// WB green darkened by 18%
|
|
93
|
+
backgroundColor: "#00880b",
|
|
85
94
|
},
|
|
86
95
|
incorrect: {
|
|
87
96
|
backgroundColor: Color.offBlack64,
|