@khanacademy/perseus 0.4.3 → 1.1.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 +6 -1
- package/CHANGELOG.md +45 -0
- package/dist/es/index.css +15 -8
- package/dist/es/index.js +1214 -1404
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +15 -8
- package/dist/index.js +1217 -1411
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -4
- package/package.json +9 -8
- package/src/components/number-input.jsx +2 -2
- package/src/components/simple-keypad-input.jsx +18 -21
- package/src/index.js +1 -1
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
- package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
- package/src/perseus-api.jsx +10 -36
- package/src/perseus-types.js +18 -145
- package/src/styles/constants.js +0 -2
- package/src/styles/perseus-renderer.less +8 -2
- package/src/styles/variables.less +0 -2
- package/src/styles/widgets/passage.less +9 -5
- package/src/types.js +18 -35
- package/src/util/answer-types.js +2 -3
- package/src/util/math.js +3 -10
- package/src/util.js +0 -6
- package/src/version.js +1 -1
- package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
- package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
- package/src/widgets/__tests__/group_test.jsx +4 -4
- package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
- package/src/widgets/__tests__/passage_test.jsx +81 -0
- package/src/widgets/__tests__/radio/base-radio_test.jsx +291 -41
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +302 -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/__tests__/radio_test.jsx +39 -15
- package/src/widgets/definition.jsx +1 -1
- package/src/widgets/group.jsx +84 -102
- package/src/widgets/input-number.jsx +6 -1
- package/src/widgets/numeric-input.jsx +87 -80
- 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 +485 -556
- package/src/widgets/radio/choice-a11y-text.js +62 -0
- package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -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 +319 -347
- 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,106 @@ 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
|
|
21
|
+
import getA11yText from "./choice-a11y-text.js";
|
|
22
|
+
import ChoiceIcon from "./choice-icon/choice-icon.jsx";
|
|
21
23
|
import OptionStatus from "./option-status.jsx";
|
|
22
24
|
import {getChoiceLetter} from "./util.js";
|
|
23
25
|
|
|
26
|
+
import type {APIOptions} from "../../types.js";
|
|
27
|
+
|
|
24
28
|
const intermediateCheckboxPadding = `16px 16px`;
|
|
25
29
|
const intermediateCheckboxPaddingPhone = `12px 16px`;
|
|
26
30
|
|
|
27
|
-
export const TouchIgnoreTimeout = 10;
|
|
28
|
-
|
|
29
31
|
const ellipsisHorizontalIcon = {
|
|
30
32
|
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
33
|
width: 100,
|
|
32
34
|
height: 27.284,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
export type ChoiceProps = {|
|
|
38
|
+
apiOptions: APIOptions,
|
|
39
|
+
checked: boolean,
|
|
40
|
+
rationale: React.Node,
|
|
41
|
+
content: React.Node,
|
|
42
|
+
correct: boolean,
|
|
43
|
+
disabled: boolean,
|
|
44
|
+
// This indicates the position of the choice relative to others
|
|
45
|
+
// (so that we can display a nice little (A), (B), etc. next to it)
|
|
46
|
+
// Also used to generate an id for each input.
|
|
47
|
+
pos: number,
|
|
48
|
+
reviewMode: boolean,
|
|
49
|
+
showRationale: boolean,
|
|
50
|
+
showCorrectness: boolean,
|
|
51
|
+
multipleSelect: boolean,
|
|
52
|
+
|
|
53
|
+
// Indicates whether the user has "crossed out" this choice, meaning
|
|
54
|
+
// that they don't think it's correct. This value does not affect
|
|
55
|
+
// scoring or other behavior; it's just a note for the user's
|
|
56
|
+
// reference.
|
|
57
|
+
crossedOut: boolean,
|
|
58
|
+
|
|
59
|
+
// Indicates that the user has previously selected this answer. These
|
|
60
|
+
// answers may be rendered orange in review, rather than grey if
|
|
61
|
+
// incorrect.
|
|
62
|
+
previouslyAnswered: boolean,
|
|
63
|
+
|
|
64
|
+
// A callback indicating that this choice has changed. Its argument is
|
|
65
|
+
// an object with two keys: `checked` and `crossedOut`. Each contains a
|
|
66
|
+
// boolean value specifying the new checked and crossed-out value of
|
|
67
|
+
// this choice.
|
|
68
|
+
onChange: (newValues: {checked: boolean, crossedOut: boolean}) => void,
|
|
38
69
|
|};
|
|
39
70
|
|
|
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
|
-
}
|
|
132
|
-
|
|
133
|
-
inputRef: ($FlowFixMe) => void = (ref) => {
|
|
134
|
-
this._input = ref;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
renderOptionStatus: () => React.Node = () => {
|
|
138
|
-
const {correct, checked, crossedOut, reviewMode, previouslyAnswered} =
|
|
139
|
-
this.props;
|
|
71
|
+
type WithForwardRef = {|forwardedRef: React.Ref<"button">|};
|
|
140
72
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
73
|
+
type ChoicePropsWithForwardRef = {|
|
|
74
|
+
...ChoiceProps,
|
|
75
|
+
...WithForwardRef,
|
|
76
|
+
|};
|
|
145
77
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
78
|
+
// Note(TB): Received errors when using useUniqueIdWithMock
|
|
79
|
+
// so created this workaround function. Will update when
|
|
80
|
+
// useUniqueIdWithMock is available.
|
|
81
|
+
// https://khanacademy.atlassian.net/browse/FEI-4861?atlOrigin=eyJpIjoiNDJlZWMwNjM1NWJhNDBkMWFjY2FmN2I0ZjcxZmQxOGUiLCJwIjoiaiJ9
|
|
82
|
+
let id = 0;
|
|
83
|
+
function uniqueId() {
|
|
84
|
+
return `choice-${id++}`;
|
|
85
|
+
}
|
|
151
86
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
87
|
+
function Choice(props: ChoicePropsWithForwardRef): React.Node {
|
|
88
|
+
const {
|
|
89
|
+
disabled,
|
|
90
|
+
checked,
|
|
91
|
+
content,
|
|
92
|
+
crossedOut,
|
|
93
|
+
showCorrectness,
|
|
94
|
+
multipleSelect,
|
|
95
|
+
onChange,
|
|
96
|
+
reviewMode,
|
|
97
|
+
correct,
|
|
98
|
+
apiOptions,
|
|
99
|
+
previouslyAnswered,
|
|
100
|
+
pos,
|
|
101
|
+
showRationale,
|
|
102
|
+
rationale,
|
|
103
|
+
forwardedRef,
|
|
104
|
+
} = props;
|
|
105
|
+
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (isInputFocused && disabled) {
|
|
109
|
+
setIsInputFocused(false);
|
|
175
110
|
}
|
|
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
|
-
};
|
|
111
|
+
}, [disabled, isInputFocused, setIsInputFocused]);
|
|
193
112
|
|
|
194
113
|
// Call `this.props.onChange` with the given values. Any keys that are not
|
|
195
114
|
// specified will be filled in with the current value. (For example, if
|
|
@@ -198,217 +117,266 @@ class Choice extends React.Component<$FlowFixMe, State> {
|
|
|
198
117
|
//
|
|
199
118
|
// This enables us to use shorthand inside this component, while
|
|
200
119
|
// maintaining a consistent API for the parent.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
120
|
+
function sendChange(newValues: {|
|
|
121
|
+
checked?: boolean,
|
|
122
|
+
crossedOut?: boolean,
|
|
123
|
+
|}) {
|
|
124
|
+
const updatedChecked = newValues.checked ?? checked;
|
|
125
|
+
const updatedCrossedOut = newValues.crossedOut ?? crossedOut;
|
|
126
|
+
onChange({checked: updatedChecked, crossedOut: updatedCrossedOut});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// HACK: while most of the styling for rendering SAT items is handled
|
|
130
|
+
// via aphrodite, we also need to assign normal CSS classnames here to
|
|
131
|
+
// special-case the coloring of MathJax formulas (see .MathJax .math in
|
|
132
|
+
// stylesheets/task-package/tasks.less)
|
|
133
|
+
const satCorrectChoice = apiOptions.satStyling && reviewMode && correct;
|
|
134
|
+
const satIncorrectChecked =
|
|
135
|
+
apiOptions.satStyling && reviewMode && !correct && checked;
|
|
136
|
+
const descriptionClassName = classNames(
|
|
137
|
+
"description",
|
|
138
|
+
satCorrectChoice && "sat-correct",
|
|
139
|
+
satIncorrectChecked && "sat-incorrect",
|
|
140
|
+
css(
|
|
141
|
+
!apiOptions.satStyling && styles.description,
|
|
142
|
+
apiOptions.satStyling && styles.satDescription,
|
|
143
|
+
satCorrectChoice && styles.satDescriptionCorrect,
|
|
144
|
+
satCorrectChoice && checked && styles.satDescriptionCorrectChecked,
|
|
145
|
+
satIncorrectChecked && styles.satDescriptionIncorrectChecked,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const rationaleClassName = classNames(
|
|
150
|
+
"perseus-radio-rationale-content",
|
|
151
|
+
css(
|
|
152
|
+
styles.rationale,
|
|
153
|
+
!apiOptions.satStyling && styles.nonSatRationale,
|
|
154
|
+
apiOptions.satStyling && styles.satReviewRationale,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// We want to show the choices as dimmed out when the choices are
|
|
159
|
+
// disabled. However, we don't want to do this in the SAT product and
|
|
160
|
+
// we also don't want to do this when we're in review mode in the
|
|
161
|
+
// content library.
|
|
162
|
+
const showDimmed =
|
|
163
|
+
(!apiOptions.satStyling && !reviewMode && apiOptions.readOnly) ||
|
|
164
|
+
crossedOut;
|
|
165
|
+
|
|
166
|
+
const letter = getChoiceLetter(pos);
|
|
167
|
+
const a11yText = getA11yText(
|
|
168
|
+
letter,
|
|
169
|
+
checked,
|
|
170
|
+
correct,
|
|
171
|
+
crossedOut,
|
|
172
|
+
showCorrectness,
|
|
173
|
+
);
|
|
174
|
+
const choiceId = uniqueId();
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
dispay: "flex",
|
|
180
|
+
flexDirection: "column",
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
259
183
|
<div
|
|
260
184
|
style={{
|
|
261
|
-
|
|
262
|
-
flexDirection: "
|
|
185
|
+
display: "flex",
|
|
186
|
+
flexDirection: "row",
|
|
187
|
+
width: "100%",
|
|
188
|
+
opacity: showDimmed ? 0.5 : 1.0,
|
|
263
189
|
}}
|
|
264
190
|
>
|
|
265
|
-
<div
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
opacity: showDimmed ? 0.5 : 1.0,
|
|
271
|
-
}}
|
|
272
|
-
>
|
|
273
|
-
<Clickable
|
|
191
|
+
<div className="perseus-sr-only">
|
|
192
|
+
<input
|
|
193
|
+
type={multipleSelect ? "checkbox" : "radio"}
|
|
194
|
+
id={choiceId}
|
|
195
|
+
checked={checked}
|
|
274
196
|
onClick={() => {
|
|
275
197
|
// If we're checking a crossed-out option, let's
|
|
276
198
|
// also uncross it.
|
|
277
|
-
|
|
278
|
-
checked:
|
|
199
|
+
sendChange({
|
|
200
|
+
checked: !checked,
|
|
279
201
|
crossedOut: false,
|
|
280
202
|
});
|
|
281
203
|
}}
|
|
282
|
-
|
|
204
|
+
onChange={() => {}}
|
|
283
205
|
disabled={
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
206
|
+
disabled ||
|
|
207
|
+
apiOptions.staticRender ||
|
|
208
|
+
apiOptions.readOnly
|
|
287
209
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
210
|
+
tabIndex={-1}
|
|
211
|
+
/>
|
|
212
|
+
<label htmlFor={choiceId}>
|
|
213
|
+
{a11yText} {content}
|
|
214
|
+
</label>
|
|
215
|
+
</div>
|
|
216
|
+
<Clickable
|
|
217
|
+
onClick={() => {
|
|
218
|
+
// If we're checking a crossed-out option, let's
|
|
219
|
+
// also uncross it.
|
|
220
|
+
sendChange({
|
|
221
|
+
checked: !checked,
|
|
222
|
+
crossedOut: false,
|
|
223
|
+
});
|
|
224
|
+
}}
|
|
225
|
+
className={descriptionClassName}
|
|
226
|
+
disabled={
|
|
227
|
+
disabled ||
|
|
228
|
+
apiOptions.staticRender ||
|
|
229
|
+
apiOptions.readOnly
|
|
230
|
+
}
|
|
231
|
+
style={{flex: 1}}
|
|
232
|
+
ref={(forwardedRef: any)}
|
|
233
|
+
aria-hidden="true"
|
|
234
|
+
>
|
|
235
|
+
{({hovered, focused, pressed}) => (
|
|
236
|
+
<div
|
|
237
|
+
style={{
|
|
238
|
+
display: "flex",
|
|
239
|
+
flexDirection: "row",
|
|
240
|
+
justifyContent: "center",
|
|
241
|
+
alignContent: "center",
|
|
242
|
+
paddingTop: Spacing.xSmall_8,
|
|
243
|
+
paddingBottom: Spacing.xSmall_8,
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<ChoiceIcon
|
|
247
|
+
pos={pos}
|
|
248
|
+
correct={correct}
|
|
249
|
+
crossedOut={crossedOut}
|
|
250
|
+
pressed={pressed}
|
|
251
|
+
focused={focused}
|
|
252
|
+
checked={checked}
|
|
253
|
+
hovered={hovered}
|
|
254
|
+
showCorrectness={showCorrectness}
|
|
255
|
+
multipleSelect={multipleSelect}
|
|
256
|
+
reviewMode={reviewMode}
|
|
257
|
+
product={
|
|
258
|
+
apiOptions.satStyling ? "sat" : "library"
|
|
259
|
+
}
|
|
260
|
+
primaryProductColor={
|
|
261
|
+
apiOptions.styling?.primaryProductColor
|
|
262
|
+
}
|
|
263
|
+
previouslyAnswered={previouslyAnswered}
|
|
264
|
+
/>
|
|
265
|
+
<span
|
|
297
266
|
style={{
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
paddingTop: Spacing.xSmall_8,
|
|
303
|
-
paddingBottom: Spacing.xSmall_8,
|
|
267
|
+
paddingLeft: Spacing.small_12,
|
|
268
|
+
textAlign: "left",
|
|
269
|
+
flex: 1,
|
|
270
|
+
paddingTop: 4,
|
|
304
271
|
}}
|
|
305
272
|
>
|
|
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>
|
|
273
|
+
<div>
|
|
274
|
+
<OptionStatus
|
|
275
|
+
checked={checked}
|
|
276
|
+
correct={correct}
|
|
277
|
+
crossedOut={crossedOut}
|
|
278
|
+
previouslyAnswered={previouslyAnswered}
|
|
279
|
+
reviewMode={reviewMode}
|
|
280
|
+
satStyling={apiOptions.satStyling}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
<div>{content}</div>
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
396
286
|
)}
|
|
397
|
-
</
|
|
398
|
-
|
|
399
|
-
{
|
|
400
|
-
<
|
|
401
|
-
|
|
402
|
-
|
|
287
|
+
</Clickable>
|
|
288
|
+
|
|
289
|
+
{apiOptions.crossOutEnabled && !reviewMode && (
|
|
290
|
+
<Popover
|
|
291
|
+
dismissEnabled
|
|
292
|
+
content={({close}) => (
|
|
293
|
+
<PopoverContent
|
|
294
|
+
title="Cross out"
|
|
295
|
+
content="Cross out option"
|
|
296
|
+
closeButtonVisible
|
|
297
|
+
actions={
|
|
298
|
+
<View>
|
|
299
|
+
<Strut size={Spacing.medium_16} />
|
|
300
|
+
<Button
|
|
301
|
+
kind="primary"
|
|
302
|
+
aria-label={`Cross out Choice ${getChoiceLetter(
|
|
303
|
+
pos,
|
|
304
|
+
)}`}
|
|
305
|
+
disabled={
|
|
306
|
+
apiOptions.readOnly ||
|
|
307
|
+
reviewMode
|
|
308
|
+
}
|
|
309
|
+
onClick={() => {
|
|
310
|
+
if (!crossedOut) {
|
|
311
|
+
// If we're crossing
|
|
312
|
+
// out a checked
|
|
313
|
+
// option, let's also
|
|
314
|
+
// uncheck it.
|
|
315
|
+
sendChange({
|
|
316
|
+
checked: false,
|
|
317
|
+
crossedOut: true,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
sendChange({
|
|
321
|
+
crossedOut: false,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
close();
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{crossedOut
|
|
328
|
+
? "Bring back"
|
|
329
|
+
: "Cross out"}
|
|
330
|
+
</Button>
|
|
331
|
+
</View>
|
|
332
|
+
}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
403
335
|
>
|
|
404
|
-
{
|
|
405
|
-
|
|
336
|
+
{({open}) => (
|
|
337
|
+
<Clickable
|
|
338
|
+
onClick={open}
|
|
339
|
+
aria-label={`Open menu for Choice ${getChoiceLetter(
|
|
340
|
+
pos,
|
|
341
|
+
)}`}
|
|
342
|
+
disabled={apiOptions.staticRender}
|
|
343
|
+
>
|
|
344
|
+
{({hovered, focused, pressed}) => (
|
|
345
|
+
<Icon
|
|
346
|
+
icon={ellipsisHorizontalIcon}
|
|
347
|
+
size={3}
|
|
348
|
+
color={Color.offBlack64}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
</Clickable>
|
|
352
|
+
)}
|
|
353
|
+
</Popover>
|
|
406
354
|
)}
|
|
407
355
|
</div>
|
|
408
|
-
|
|
409
|
-
|
|
356
|
+
|
|
357
|
+
{showRationale && (
|
|
358
|
+
<div
|
|
359
|
+
className={rationaleClassName}
|
|
360
|
+
data-test-id={`perseus-radio-rationale-content-${pos}`}
|
|
361
|
+
>
|
|
362
|
+
{rationale}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
410
367
|
}
|
|
411
368
|
|
|
369
|
+
Choice.defaultProps = {
|
|
370
|
+
disabled: false,
|
|
371
|
+
checked: false,
|
|
372
|
+
onChange: (newValues: {checked: boolean, crossedOut: boolean}): void => {},
|
|
373
|
+
correct: false,
|
|
374
|
+
apiOptions: ApiOptions.defaults,
|
|
375
|
+
previouslyAnswered: false,
|
|
376
|
+
pos: 0,
|
|
377
|
+
showRationale: false,
|
|
378
|
+
};
|
|
379
|
+
|
|
412
380
|
const styles = StyleSheet.create({
|
|
413
381
|
description: {
|
|
414
382
|
display: "inline-block",
|
|
@@ -457,4 +425,8 @@ const styles = StyleSheet.create({
|
|
|
457
425
|
},
|
|
458
426
|
});
|
|
459
427
|
|
|
460
|
-
|
|
428
|
+
type ExportProps = $Diff<React.ElementConfig<typeof Choice>, WithForwardRef>;
|
|
429
|
+
|
|
430
|
+
export default (React.forwardRef<ExportProps, HTMLButtonElement>(
|
|
431
|
+
(props, ref) => <Choice {...props} forwardedRef={ref} />,
|
|
432
|
+
): React.AbstractComponent<ExportProps, HTMLButtonElement>);
|