@khanacademy/perseus 0.4.2 → 1.0.0
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/.eslintrc.js +11 -0
- package/CHANGELOG.md +47 -0
- package/dist/es/index.js +1078 -1299
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +1077 -1304
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -4
- package/package.json +9 -8
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +110 -110
- package/src/perseus-api.jsx +10 -36
- package/src/perseus-types.js +0 -138
- package/src/styles/constants.js +0 -2
- package/src/styles/variables.less +0 -2
- package/src/types.js +18 -35
- package/src/util.js +0 -6
- package/src/version.js +1 -1
- package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +110 -110
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +1136 -1160
- package/src/widgets/__tests__/radio/base-radio_test.jsx +243 -41
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +201 -0
- package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
- package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
- package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
- package/src/widgets/passage/get-line-height-for-node.js +4 -5
- package/src/widgets/passage/passage-markdown.jsx +176 -120
- package/src/widgets/passage.jsx +48 -46
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
- package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
- package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
- package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
- package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
- package/src/widgets/radio/base-radio.jsx +483 -556
- package/src/widgets/radio/choice-icon/choice-icon.jsx +155 -0
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +219 -0
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +129 -0
- package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
- package/src/widgets/radio/choice-none-above.jsx +49 -40
- package/src/widgets/radio/choice.jsx +282 -353
- package/src/widgets/radio/choice.jsx.fixture.js +0 -18
- package/src/widgets/radio/focus-ring.jsx +29 -22
- package/src/widgets/radio/option-status.jsx +58 -36
- package/src/widgets/radio/widget.jsx +181 -136
- package/src/widgets/radio.jsx +16 -17
- package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
- package/src/widgets/radio/choice-icon.jsx +0 -469
- package/src/widgets/radio/cross-out-button.jsx +0 -134
|
@@ -9,187 +9,96 @@ import {Popover, PopoverContent} from "@khanacademy/wonder-blocks-popover";
|
|
|
9
9
|
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
10
10
|
import {StyleSheet, css} from "aphrodite";
|
|
11
11
|
import classNames from "classnames";
|
|
12
|
-
import PropTypes from "prop-types";
|
|
13
12
|
import * as React from "react";
|
|
13
|
+
import {useState, useEffect} from "react";
|
|
14
14
|
import _ from "underscore";
|
|
15
15
|
|
|
16
16
|
import Icon from "../../components/icon.jsx";
|
|
17
|
+
import {ApiOptions} from "../../perseus-api.jsx";
|
|
17
18
|
import * as styleConstants from "../../styles/constants.js";
|
|
18
19
|
import mediaQueries from "../../styles/media-queries.js";
|
|
19
20
|
|
|
20
|
-
import ChoiceIcon from "./choice-icon.jsx";
|
|
21
|
+
import ChoiceIcon from "./choice-icon/choice-icon.jsx";
|
|
21
22
|
import OptionStatus from "./option-status.jsx";
|
|
22
23
|
import {getChoiceLetter} from "./util.js";
|
|
23
24
|
|
|
25
|
+
import type {APIOptions} from "../../types.js";
|
|
26
|
+
|
|
24
27
|
const intermediateCheckboxPadding = `16px 16px`;
|
|
25
28
|
const intermediateCheckboxPaddingPhone = `12px 16px`;
|
|
26
29
|
|
|
27
|
-
export const TouchIgnoreTimeout = 10;
|
|
28
|
-
|
|
29
30
|
const ellipsisHorizontalIcon = {
|
|
30
31
|
path: "M27.218 6.82l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836zm36.27 0l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836zm36.27 0l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836z",
|
|
31
32
|
width: 100,
|
|
32
33
|
height: 27.284,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
export type ChoiceProps = {|
|
|
37
|
+
apiOptions: APIOptions,
|
|
38
|
+
checked: boolean,
|
|
39
|
+
rationale: React.Node,
|
|
40
|
+
content: React.Node,
|
|
41
|
+
correct: boolean,
|
|
42
|
+
disabled: boolean,
|
|
43
|
+
// This indicates the position of the choice relative to others
|
|
44
|
+
// (so that we can display a nice little (A), (B), etc. next to it)
|
|
45
|
+
// Also used to generate an id for each input.
|
|
46
|
+
pos: number,
|
|
47
|
+
reviewMode: boolean,
|
|
48
|
+
showRationale: boolean,
|
|
49
|
+
showCorrectness: boolean,
|
|
50
|
+
multipleSelect: boolean,
|
|
51
|
+
|
|
52
|
+
// Indicates whether the user has "crossed out" this choice, meaning
|
|
53
|
+
// that they don't think it's correct. This value does not affect
|
|
54
|
+
// scoring or other behavior; it's just a note for the user's
|
|
55
|
+
// reference.
|
|
56
|
+
crossedOut: boolean,
|
|
57
|
+
|
|
58
|
+
// Indicates that the user has previously selected this answer. These
|
|
59
|
+
// answers may be rendered orange in review, rather than grey if
|
|
60
|
+
// incorrect.
|
|
61
|
+
previouslyAnswered: boolean,
|
|
62
|
+
|
|
63
|
+
// A callback indicating that this choice has changed. Its argument is
|
|
64
|
+
// an object with two keys: `checked` and `crossedOut`. Each contains a
|
|
65
|
+
// boolean value specifying the new checked and crossed-out value of
|
|
66
|
+
// this choice.
|
|
67
|
+
onChange: (newValues: {checked: boolean, crossedOut: boolean}) => void,
|
|
38
68
|
|};
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
justFinishedTouch: boolean;
|
|
42
|
-
justFinishedTimeoutID: TimeoutID;
|
|
43
|
-
_input: ?HTMLInputElement;
|
|
44
|
-
|
|
45
|
-
static propTypes = {
|
|
46
|
-
// TODO(kevinb) use Options.propTypes from perseus-api.jsx
|
|
47
|
-
// This change will also require make sure that item-renderer.jsx and
|
|
48
|
-
// server-item-renderer.jsx have appropriate defaults for apiOptions
|
|
49
|
-
// because many of the properties on Options.propTypes are required.
|
|
50
|
-
apiOptions: PropTypes.shape({
|
|
51
|
-
satStyling: PropTypes.bool,
|
|
52
|
-
isMobile: PropTypes.bool,
|
|
53
|
-
styling: PropTypes.shape({
|
|
54
|
-
radioStyleVersion: PropTypes.oneOf(["intermediate", "final"]),
|
|
55
|
-
primaryProductColor: PropTypes.string,
|
|
56
|
-
}),
|
|
57
|
-
readOnly: PropTypes.bool,
|
|
58
|
-
}),
|
|
59
|
-
checked: PropTypes.bool,
|
|
60
|
-
className: PropTypes.string,
|
|
61
|
-
rationale: PropTypes.node,
|
|
62
|
-
content: PropTypes.node,
|
|
63
|
-
correct: PropTypes.bool,
|
|
64
|
-
deselectEnabled: PropTypes.bool,
|
|
65
|
-
disabled: PropTypes.bool,
|
|
66
|
-
editMode: PropTypes.bool,
|
|
67
|
-
groupName: PropTypes.string,
|
|
68
|
-
isLastChoice: PropTypes.bool, // Needed for border styling
|
|
69
|
-
// This indicates the position of the choice relative to others
|
|
70
|
-
// (so that we can display a nice little (A), (B), etc. next to it)
|
|
71
|
-
// Also used to generate an id for each input.
|
|
72
|
-
pos: PropTypes.number,
|
|
73
|
-
reviewMode: PropTypes.bool,
|
|
74
|
-
showRationale: PropTypes.bool,
|
|
75
|
-
showCorrectness: PropTypes.bool,
|
|
76
|
-
type: PropTypes.string,
|
|
77
|
-
|
|
78
|
-
// Indicates whether the user has "crossed out" this choice, meaning
|
|
79
|
-
// that they don't think it's correct. This value does not affect
|
|
80
|
-
// scoring or other behavior; it's just a note for the user's
|
|
81
|
-
// reference.
|
|
82
|
-
crossedOut: PropTypes.bool,
|
|
83
|
-
|
|
84
|
-
// Indicates that the user has previously selected this answer. These
|
|
85
|
-
// answers may be rendered orange in review, rather than grey if
|
|
86
|
-
// incorrect.
|
|
87
|
-
previouslyAnswered: PropTypes.bool,
|
|
88
|
-
|
|
89
|
-
// A callback indicating that this choice has changed. Its argument is
|
|
90
|
-
// an object with two keys: `checked` and `crossedOut`. Each contains a
|
|
91
|
-
// boolean value specifying the new checked and crossed-out value of
|
|
92
|
-
// this choice.
|
|
93
|
-
onChange: PropTypes.func,
|
|
94
|
-
|
|
95
|
-
// Callbacks for when the user presses the up arrow or down arrow
|
|
96
|
-
// (respectively) to navigate between choices. When called, the parent
|
|
97
|
-
// finds the prev/next choice, focuses the input, and checks the radio
|
|
98
|
-
// button if appropriate. (The reason we have custom behavior is that
|
|
99
|
-
// we actually _don't_ want to auto-select crossed-out choices!)
|
|
100
|
-
goToPrevChoice: PropTypes.func.isRequired,
|
|
101
|
-
goToNextChoice: PropTypes.func.isRequired,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
static defaultProps: $FlowFixMe = {
|
|
105
|
-
apiOptions: {
|
|
106
|
-
styling: {},
|
|
107
|
-
},
|
|
108
|
-
checked: false,
|
|
109
|
-
classSet: {},
|
|
110
|
-
correct: false,
|
|
111
|
-
disabled: false,
|
|
112
|
-
editMode: false,
|
|
113
|
-
onChange: function () {},
|
|
114
|
-
showRationale: false,
|
|
115
|
-
type: "radio",
|
|
116
|
-
pos: 0,
|
|
117
|
-
previouslyAnswered: false,
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
state: $FlowFixMe = {
|
|
121
|
-
isInputFocused: false,
|
|
122
|
-
isInputActive: false,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
UNSAFE_componentWillUpdate(nextProps: $FlowFixMe) {
|
|
126
|
-
if (this.state.isInputFocused && nextProps.disabled) {
|
|
127
|
-
this.setState({
|
|
128
|
-
isInputFocused: false,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
70
|
+
type WithForwardRef = {|forwardedRef: React.Ref<"button">|};
|
|
132
71
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
renderOptionStatus: () => React.Node = () => {
|
|
138
|
-
const {correct, checked, crossedOut, reviewMode, previouslyAnswered} =
|
|
139
|
-
this.props;
|
|
140
|
-
|
|
141
|
-
// Option status is exclued for SAT
|
|
142
|
-
if (this.props.apiOptions.satStyling) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Option status is shown only in review mode, or for incorrectly
|
|
147
|
-
// answered items.
|
|
148
|
-
if (!reviewMode && !previouslyAnswered) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<OptionStatus
|
|
154
|
-
checked={checked}
|
|
155
|
-
correct={correct}
|
|
156
|
-
crossedOut={crossedOut}
|
|
157
|
-
previouslyAnswered={previouslyAnswered}
|
|
158
|
-
/>
|
|
159
|
-
);
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
renderChoiceIcon: (args: {|
|
|
163
|
-
isFocused: boolean,
|
|
164
|
-
isPressed: boolean,
|
|
165
|
-
|}) => React.Node = ({isFocused, isPressed}) => {
|
|
166
|
-
const {radioStyleVersion, primaryProductColor} =
|
|
167
|
-
this.props.apiOptions.styling;
|
|
168
|
-
const finalStyles =
|
|
169
|
-
typeof radioStyleVersion === "undefined"
|
|
170
|
-
? false
|
|
171
|
-
: radioStyleVersion === "final";
|
|
72
|
+
type ChoicePropsWithForwardRef = {|
|
|
73
|
+
...ChoiceProps,
|
|
74
|
+
...WithForwardRef,
|
|
75
|
+
|};
|
|
172
76
|
|
|
173
|
-
|
|
174
|
-
|
|
77
|
+
function Choice(props: ChoicePropsWithForwardRef): React.Node {
|
|
78
|
+
const {
|
|
79
|
+
disabled,
|
|
80
|
+
checked,
|
|
81
|
+
content,
|
|
82
|
+
crossedOut,
|
|
83
|
+
showCorrectness,
|
|
84
|
+
multipleSelect,
|
|
85
|
+
onChange,
|
|
86
|
+
reviewMode,
|
|
87
|
+
correct,
|
|
88
|
+
apiOptions,
|
|
89
|
+
previouslyAnswered,
|
|
90
|
+
pos,
|
|
91
|
+
showRationale,
|
|
92
|
+
rationale,
|
|
93
|
+
forwardedRef,
|
|
94
|
+
} = props;
|
|
95
|
+
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (isInputFocused && disabled) {
|
|
99
|
+
setIsInputFocused(false);
|
|
175
100
|
}
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<ChoiceIcon
|
|
179
|
-
pos={this.props.pos}
|
|
180
|
-
correct={this.props.correct}
|
|
181
|
-
crossedOut={this.props.crossedOut}
|
|
182
|
-
pressed={isPressed}
|
|
183
|
-
focused={isFocused}
|
|
184
|
-
checked={this.props.checked}
|
|
185
|
-
showCorrectness={this.props.showCorrectness}
|
|
186
|
-
reviewMode={this.props.reviewMode}
|
|
187
|
-
product={this.props.apiOptions.satStyling ? "sat" : "library"}
|
|
188
|
-
primaryProductColor={primaryProductColor}
|
|
189
|
-
previouslyAnswered={this.props.previouslyAnswered}
|
|
190
|
-
/>
|
|
191
|
-
);
|
|
192
|
-
};
|
|
101
|
+
}, [disabled, isInputFocused, setIsInputFocused]);
|
|
193
102
|
|
|
194
103
|
// Call `this.props.onChange` with the given values. Any keys that are not
|
|
195
104
|
// specified will be filled in with the current value. (For example, if
|
|
@@ -198,217 +107,233 @@ class Choice extends React.Component<$FlowFixMe, State> {
|
|
|
198
107
|
//
|
|
199
108
|
// This enables us to use shorthand inside this component, while
|
|
200
109
|
// maintaining a consistent API for the parent.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Public method. Focus the choice's <input> element.
|
|
211
|
-
*/
|
|
212
|
-
focusInput: () => void = () => {
|
|
213
|
-
this._input?.focus();
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
render(): React.Node {
|
|
217
|
-
const sat = this.props.apiOptions.satStyling;
|
|
218
|
-
|
|
219
|
-
const {reviewMode, correct, checked} = this.props;
|
|
220
|
-
// HACK: while most of the styling for rendering SAT items is handled
|
|
221
|
-
// via aphrodite, we also need to assign normal CSS classnames here to
|
|
222
|
-
// special-case the coloring of MathJax formulas (see .MathJax .math in
|
|
223
|
-
// stylesheets/task-package/tasks.less)
|
|
224
|
-
const satCorrectChoice = sat && reviewMode && correct;
|
|
225
|
-
const satIncorrectChecked = sat && reviewMode && !correct && checked;
|
|
226
|
-
const descriptionClassName = classNames(
|
|
227
|
-
"description",
|
|
228
|
-
satCorrectChoice && "sat-correct",
|
|
229
|
-
satIncorrectChecked && "sat-incorrect",
|
|
230
|
-
css(
|
|
231
|
-
!sat && styles.description,
|
|
232
|
-
sat && styles.satDescription,
|
|
233
|
-
satCorrectChoice && styles.satDescriptionCorrect,
|
|
234
|
-
satCorrectChoice &&
|
|
235
|
-
checked &&
|
|
236
|
-
styles.satDescriptionCorrectChecked,
|
|
237
|
-
satIncorrectChecked && styles.satDescriptionIncorrectChecked,
|
|
238
|
-
),
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
const rationaleClassName = classNames(
|
|
242
|
-
"perseus-radio-rationale-content",
|
|
243
|
-
css(
|
|
244
|
-
styles.rationale,
|
|
245
|
-
!sat && styles.nonSatRationale,
|
|
246
|
-
sat && styles.satReviewRationale,
|
|
247
|
-
),
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// We want to show the choices as dimmed out when the choices are
|
|
251
|
-
// disabled. However, we don't want to do this in the SAT product and
|
|
252
|
-
// we also don't want to do this when we're in review mode in the
|
|
253
|
-
// content library.
|
|
254
|
-
const showDimmed =
|
|
255
|
-
(!sat && !reviewMode && this.props.apiOptions.readOnly) ||
|
|
256
|
-
this.props.crossedOut;
|
|
110
|
+
function sendChange(newValues: {|
|
|
111
|
+
checked?: boolean,
|
|
112
|
+
crossedOut?: boolean,
|
|
113
|
+
|}) {
|
|
114
|
+
const updatedChecked = newValues.checked ?? checked;
|
|
115
|
+
const updatedCrossedOut = newValues.crossedOut ?? crossedOut;
|
|
116
|
+
onChange({checked: updatedChecked, crossedOut: updatedCrossedOut});
|
|
117
|
+
}
|
|
257
118
|
|
|
258
|
-
|
|
119
|
+
// HACK: while most of the styling for rendering SAT items is handled
|
|
120
|
+
// via aphrodite, we also need to assign normal CSS classnames here to
|
|
121
|
+
// special-case the coloring of MathJax formulas (see .MathJax .math in
|
|
122
|
+
// stylesheets/task-package/tasks.less)
|
|
123
|
+
const satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
|
|
124
|
+
const satIncorrectChecked =
|
|
125
|
+
apiOptions.satStyling && reviewMode && !correct && checked;
|
|
126
|
+
const descriptionClassName = classNames(
|
|
127
|
+
"description",
|
|
128
|
+
satCorrectChoice && "sat-correct",
|
|
129
|
+
satIncorrectChecked && "sat-incorrect",
|
|
130
|
+
css(
|
|
131
|
+
!apiOptions.satStyling && styles.description,
|
|
132
|
+
apiOptions.satStyling && styles.satDescription,
|
|
133
|
+
satCorrectChoice && styles.satDescriptionCorrect,
|
|
134
|
+
satCorrectChoice && checked && styles.satDescriptionCorrectChecked,
|
|
135
|
+
satIncorrectChecked && styles.satDescriptionIncorrectChecked,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const rationaleClassName = classNames(
|
|
140
|
+
"perseus-radio-rationale-content",
|
|
141
|
+
css(
|
|
142
|
+
styles.rationale,
|
|
143
|
+
!apiOptions.satStyling && styles.nonSatRationale,
|
|
144
|
+
apiOptions.satStyling && styles.satReviewRationale,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// We want to show the choices as dimmed out when the choices are
|
|
149
|
+
// disabled. However, we don't want to do this in the SAT product and
|
|
150
|
+
// we also don't want to do this when we're in review mode in the
|
|
151
|
+
// content library.
|
|
152
|
+
const showDimmed =
|
|
153
|
+
(!apiOptions.satStyling && !reviewMode && apiOptions.readOnly) ||
|
|
154
|
+
crossedOut;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
style={{
|
|
159
|
+
dispay: "flex",
|
|
160
|
+
flexDirection: "column",
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
259
163
|
<div
|
|
260
164
|
style={{
|
|
261
|
-
|
|
262
|
-
flexDirection: "
|
|
165
|
+
display: "flex",
|
|
166
|
+
flexDirection: "row",
|
|
167
|
+
width: "100%",
|
|
168
|
+
opacity: showDimmed ? 0.5 : 1.0,
|
|
263
169
|
}}
|
|
264
170
|
>
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
171
|
+
<Clickable
|
|
172
|
+
onClick={() => {
|
|
173
|
+
// If we're checking a crossed-out option, let's
|
|
174
|
+
// also uncross it.
|
|
175
|
+
sendChange({
|
|
176
|
+
checked: !checked,
|
|
177
|
+
crossedOut: false,
|
|
178
|
+
});
|
|
271
179
|
}}
|
|
180
|
+
className={descriptionClassName}
|
|
181
|
+
disabled={
|
|
182
|
+
disabled ||
|
|
183
|
+
apiOptions.staticRender ||
|
|
184
|
+
apiOptions.readOnly
|
|
185
|
+
}
|
|
186
|
+
aria-label={`Select Choice ${getChoiceLetter(pos)}`}
|
|
187
|
+
aria-checked={checked ? "true" : "false"}
|
|
188
|
+
role={"checkbox"}
|
|
189
|
+
style={{flex: 1}}
|
|
190
|
+
ref={(forwardedRef: any)}
|
|
272
191
|
>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
192
|
+
{({hovered, focused, pressed}) => (
|
|
193
|
+
<div
|
|
194
|
+
style={{
|
|
195
|
+
display: "flex",
|
|
196
|
+
flexDirection: "row",
|
|
197
|
+
justifyContent: "center",
|
|
198
|
+
alignContent: "center",
|
|
199
|
+
paddingTop: Spacing.xSmall_8,
|
|
200
|
+
paddingBottom: Spacing.xSmall_8,
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<ChoiceIcon
|
|
204
|
+
pos={pos}
|
|
205
|
+
correct={correct}
|
|
206
|
+
crossedOut={crossedOut}
|
|
207
|
+
pressed={pressed}
|
|
208
|
+
focused={focused}
|
|
209
|
+
checked={checked}
|
|
210
|
+
hovered={hovered}
|
|
211
|
+
showCorrectness={showCorrectness}
|
|
212
|
+
multipleSelect={multipleSelect}
|
|
213
|
+
reviewMode={reviewMode}
|
|
214
|
+
product={
|
|
215
|
+
apiOptions.satStyling ? "sat" : "library"
|
|
216
|
+
}
|
|
217
|
+
primaryProductColor={
|
|
218
|
+
apiOptions.styling?.primaryProductColor
|
|
219
|
+
}
|
|
220
|
+
previouslyAnswered={previouslyAnswered}
|
|
221
|
+
/>
|
|
222
|
+
<span
|
|
297
223
|
style={{
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
paddingTop: Spacing.xSmall_8,
|
|
303
|
-
paddingBottom: Spacing.xSmall_8,
|
|
224
|
+
paddingLeft: Spacing.small_12,
|
|
225
|
+
textAlign: "left",
|
|
226
|
+
flex: 1,
|
|
227
|
+
paddingTop: 4,
|
|
304
228
|
}}
|
|
305
229
|
>
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
>
|
|
320
|
-
<div>{this.renderOptionStatus()}</div>
|
|
321
|
-
<div>{this.props.content}</div>
|
|
322
|
-
</span>
|
|
323
|
-
</div>
|
|
324
|
-
)}
|
|
325
|
-
</Clickable>
|
|
326
|
-
|
|
327
|
-
{this.props.apiOptions.crossOutEnabled && !reviewMode && (
|
|
328
|
-
<Popover
|
|
329
|
-
dismissEnabled
|
|
330
|
-
content={({close}) => (
|
|
331
|
-
<PopoverContent
|
|
332
|
-
title="Cross out"
|
|
333
|
-
content="Cross out option"
|
|
334
|
-
closeButtonVisible
|
|
335
|
-
actions={
|
|
336
|
-
<View>
|
|
337
|
-
<Strut size={Spacing.medium_16} />
|
|
338
|
-
<Button
|
|
339
|
-
kind="primary"
|
|
340
|
-
aria-label={`Cross out Choice ${getChoiceLetter(
|
|
341
|
-
this.props.pos,
|
|
342
|
-
)}`}
|
|
343
|
-
disabled={
|
|
344
|
-
this.props.apiOptions
|
|
345
|
-
.readOnly || reviewMode
|
|
346
|
-
}
|
|
347
|
-
onClick={() => {
|
|
348
|
-
if (
|
|
349
|
-
!this.props.crossedOut
|
|
350
|
-
) {
|
|
351
|
-
// If we're crossing
|
|
352
|
-
// out a checked
|
|
353
|
-
// option, let's also
|
|
354
|
-
// uncheck it.
|
|
355
|
-
this._sendChange({
|
|
356
|
-
checked: false,
|
|
357
|
-
crossedOut: true,
|
|
358
|
-
});
|
|
359
|
-
} else {
|
|
360
|
-
this._sendChange({
|
|
361
|
-
crossedOut: false,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
close();
|
|
365
|
-
}}
|
|
366
|
-
>
|
|
367
|
-
{this.props.crossedOut
|
|
368
|
-
? "Bring back"
|
|
369
|
-
: "Cross out"}
|
|
370
|
-
</Button>
|
|
371
|
-
</View>
|
|
372
|
-
}
|
|
373
|
-
/>
|
|
374
|
-
)}
|
|
375
|
-
>
|
|
376
|
-
{({open}) => (
|
|
377
|
-
<Clickable
|
|
378
|
-
onClick={open}
|
|
379
|
-
aria-label={`Open menu for Choice ${getChoiceLetter(
|
|
380
|
-
this.props.pos,
|
|
381
|
-
)}`}
|
|
382
|
-
disabled={
|
|
383
|
-
this.props.apiOptions.staticRender
|
|
384
|
-
}
|
|
385
|
-
>
|
|
386
|
-
{({hovered, focused, pressed}) => (
|
|
387
|
-
<Icon
|
|
388
|
-
icon={ellipsisHorizontalIcon}
|
|
389
|
-
size={3}
|
|
390
|
-
color={Color.offBlack64}
|
|
391
|
-
/>
|
|
392
|
-
)}
|
|
393
|
-
</Clickable>
|
|
394
|
-
)}
|
|
395
|
-
</Popover>
|
|
230
|
+
<div>
|
|
231
|
+
<OptionStatus
|
|
232
|
+
checked={checked}
|
|
233
|
+
correct={correct}
|
|
234
|
+
crossedOut={crossedOut}
|
|
235
|
+
previouslyAnswered={previouslyAnswered}
|
|
236
|
+
reviewMode={reviewMode}
|
|
237
|
+
satStyling={apiOptions.satStyling}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<div>{content}</div>
|
|
241
|
+
</span>
|
|
242
|
+
</div>
|
|
396
243
|
)}
|
|
397
|
-
</
|
|
398
|
-
|
|
399
|
-
{
|
|
400
|
-
<
|
|
401
|
-
|
|
402
|
-
|
|
244
|
+
</Clickable>
|
|
245
|
+
|
|
246
|
+
{apiOptions.crossOutEnabled && !reviewMode && (
|
|
247
|
+
<Popover
|
|
248
|
+
dismissEnabled
|
|
249
|
+
content={({close}) => (
|
|
250
|
+
<PopoverContent
|
|
251
|
+
title="Cross out"
|
|
252
|
+
content="Cross out option"
|
|
253
|
+
closeButtonVisible
|
|
254
|
+
actions={
|
|
255
|
+
<View>
|
|
256
|
+
<Strut size={Spacing.medium_16} />
|
|
257
|
+
<Button
|
|
258
|
+
kind="primary"
|
|
259
|
+
aria-label={`Cross out Choice ${getChoiceLetter(
|
|
260
|
+
pos,
|
|
261
|
+
)}`}
|
|
262
|
+
disabled={
|
|
263
|
+
apiOptions.readOnly ||
|
|
264
|
+
reviewMode
|
|
265
|
+
}
|
|
266
|
+
onClick={() => {
|
|
267
|
+
if (!crossedOut) {
|
|
268
|
+
// If we're crossing
|
|
269
|
+
// out a checked
|
|
270
|
+
// option, let's also
|
|
271
|
+
// uncheck it.
|
|
272
|
+
sendChange({
|
|
273
|
+
checked: false,
|
|
274
|
+
crossedOut: true,
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
sendChange({
|
|
278
|
+
crossedOut: false,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
close();
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
{crossedOut
|
|
285
|
+
? "Bring back"
|
|
286
|
+
: "Cross out"}
|
|
287
|
+
</Button>
|
|
288
|
+
</View>
|
|
289
|
+
}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
403
292
|
>
|
|
404
|
-
{
|
|
405
|
-
|
|
293
|
+
{({open}) => (
|
|
294
|
+
<Clickable
|
|
295
|
+
onClick={open}
|
|
296
|
+
aria-label={`Open menu for Choice ${getChoiceLetter(
|
|
297
|
+
pos,
|
|
298
|
+
)}`}
|
|
299
|
+
disabled={apiOptions.staticRender}
|
|
300
|
+
>
|
|
301
|
+
{({hovered, focused, pressed}) => (
|
|
302
|
+
<Icon
|
|
303
|
+
icon={ellipsisHorizontalIcon}
|
|
304
|
+
size={3}
|
|
305
|
+
color={Color.offBlack64}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
</Clickable>
|
|
309
|
+
)}
|
|
310
|
+
</Popover>
|
|
406
311
|
)}
|
|
407
312
|
</div>
|
|
408
|
-
|
|
409
|
-
|
|
313
|
+
|
|
314
|
+
{showRationale && (
|
|
315
|
+
<div
|
|
316
|
+
className={rationaleClassName}
|
|
317
|
+
data-test-id={`perseus-radio-rationale-content-${pos}`}
|
|
318
|
+
>
|
|
319
|
+
{rationale}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
410
324
|
}
|
|
411
325
|
|
|
326
|
+
Choice.defaultProps = {
|
|
327
|
+
disabled: false,
|
|
328
|
+
checked: false,
|
|
329
|
+
onChange: (newValues: {checked: boolean, crossedOut: boolean}): void => {},
|
|
330
|
+
correct: false,
|
|
331
|
+
apiOptions: ApiOptions.defaults,
|
|
332
|
+
previouslyAnswered: false,
|
|
333
|
+
pos: 0,
|
|
334
|
+
showRationale: false,
|
|
335
|
+
};
|
|
336
|
+
|
|
412
337
|
const styles = StyleSheet.create({
|
|
413
338
|
description: {
|
|
414
339
|
display: "inline-block",
|
|
@@ -457,4 +382,8 @@ const styles = StyleSheet.create({
|
|
|
457
382
|
},
|
|
458
383
|
});
|
|
459
384
|
|
|
460
|
-
|
|
385
|
+
type ExportProps = $Diff<React.ElementConfig<typeof Choice>, WithForwardRef>;
|
|
386
|
+
|
|
387
|
+
export default (React.forwardRef<ExportProps, HTMLButtonElement>(
|
|
388
|
+
(props, ref) => <Choice {...props} forwardedRef={ref} />,
|
|
389
|
+
): React.AbstractComponent<ExportProps, HTMLButtonElement>);
|