@khanacademy/perseus 0.3.4 → 0.4.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/es/index.js +8232 -5591
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/index.js +17876 -15237
  5. package/dist/index.js.map +1 -1
  6. package/dist/strings.js +0 -1
  7. package/package.json +9 -8
  8. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +2 -3
  9. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +2 -3
  10. package/src/__tests__/renderer-api_test.jsx +3 -27
  11. package/src/components/__stories__/svg-image.stories.jsx +11 -0
  12. package/src/components/fixed-to-responsive.jsx +1 -0
  13. package/src/components/input-with-examples.jsx +3 -1
  14. package/src/components/svg-image.jsx +10 -7
  15. package/src/components/text-input.jsx +65 -16
  16. package/src/definition-context.js +59 -0
  17. package/src/index.js +5 -1
  18. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +438 -523
  19. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  20. package/src/renderer.jsx +13 -6
  21. package/src/types.js +3 -4
  22. package/src/widgets/__stories__/definition.stories.jsx +44 -0
  23. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +76 -8
  24. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +6 -9
  25. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +2 -3
  26. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +19 -2
  27. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +338 -390
  28. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +38 -4
  29. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +114 -12
  30. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +120 -48
  31. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +8 -12
  32. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3615 -4517
  33. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +0 -1
  34. package/src/widgets/__tests__/grapher.cypress.jsx +236 -176
  35. package/src/widgets/__tests__/group_test.jsx +4 -4
  36. package/src/widgets/__tests__/interactive-graph.cypress.jsx +371 -291
  37. package/src/widgets/__tests__/radio_test.jsx +73 -114
  38. package/src/widgets/definition.jsx +34 -29
  39. package/src/widgets/numeric-input.jsx +2 -15
  40. package/src/widgets/radio/base-radio.jsx +1 -1
  41. package/src/widgets/radio/choice-icon.jsx +11 -10
  42. package/src/widgets/radio/choice.jsx +164 -396
  43. package/src/widgets/radio/cross-out-button.jsx +3 -5
  44. package/src/widgets/radio/option-status.jsx +4 -5
  45. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +0 -59
  46. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +0 -59
  47. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +0 -28
  48. package/src/widgets/radio/cross-out-menu-wrapper.jsx +0 -492
  49. package/src/widgets/radio/cross-out-tooltip.jsx +0 -129
  50. package/src/widgets/radio/toggleable-radio-button.jsx +0 -133
@@ -1,33 +1,37 @@
1
1
  /* eslint-disable react/no-unsafe */
2
2
  // @flow
3
+ import Button from "@khanacademy/wonder-blocks-button";
4
+ import Clickable from "@khanacademy/wonder-blocks-clickable";
5
+ import Color from "@khanacademy/wonder-blocks-color";
6
+ import {View} from "@khanacademy/wonder-blocks-core";
7
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
8
+ import {Popover, PopoverContent} from "@khanacademy/wonder-blocks-popover";
9
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
3
10
  import {StyleSheet, css} from "aphrodite";
4
11
  import classNames from "classnames";
5
12
  import PropTypes from "prop-types";
6
13
  import * as React from "react";
7
14
  import _ from "underscore";
8
15
 
9
- import {ClassNames} from "../../perseus-api.jsx";
16
+ import Icon from "../../components/icon.jsx";
10
17
  import * as styleConstants from "../../styles/constants.js";
11
18
  import mediaQueries from "../../styles/media-queries.js";
12
- import sharedStyles from "../../styles/shared.js";
13
19
 
14
20
  import ChoiceIcon from "./choice-icon.jsx";
15
- import CrossOutMenuWrapper from "./cross-out-menu-wrapper.jsx";
16
21
  import OptionStatus from "./option-status.jsx";
17
- import ToggleableRadioButton from "./toggleable-radio-button.jsx";
18
-
19
- const focusedStyleMixin = {
20
- backgroundColor: styleConstants.satSelectedBackgroundColor,
21
- outline: `2px solid ${styleConstants.satBlue}`,
22
- // Render the outline higher than the next element's border
23
- zIndex: 1,
24
- };
22
+ import {getChoiceLetter} from "./util.js";
25
23
 
26
24
  const intermediateCheckboxPadding = `16px 16px`;
27
25
  const intermediateCheckboxPaddingPhone = `12px 16px`;
28
26
 
29
27
  export const TouchIgnoreTimeout = 10;
30
28
 
29
+ const ellipsisHorizontalIcon = {
30
+ 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
+ width: 100,
32
+ height: 27.284,
33
+ };
34
+
31
35
  type State = {|
32
36
  isInputFocused: boolean,
33
37
  isInputActive: boolean,
@@ -126,86 +130,6 @@ class Choice extends React.Component<$FlowFixMe, State> {
126
130
  }
127
131
  }
128
132
 
129
- componentWillUnmount() {
130
- if (this.justFinishedTimeoutID != null) {
131
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
132
- // eslint-disable-next-line no-restricted-syntax
133
- clearTimeout(this.justFinishedTimeoutID);
134
- }
135
- }
136
-
137
- onInputFocus: () => void = () => {
138
- this.setState({isInputFocused: true});
139
- };
140
-
141
- onInputBlur: () => void = () => {
142
- this.setState({isInputFocused: false});
143
- };
144
-
145
- onInputMouseDown: (SyntheticMouseEvent<> | SyntheticTouchEvent<>) => void =
146
- (e) => {
147
- if (e.type === "mousedown" && this.justFinishedTouch) {
148
- return;
149
- }
150
-
151
- this.setState({isInputActive: true});
152
-
153
- // Simulate Chrome's radio button behavior in all browsers: when the
154
- // mouse goes down or up, the radio button should become focused.
155
- // That way, the newly-selected answer becomes highlighted after click.
156
- if (this.props.apiOptions.satStyling && this._input) {
157
- this._input.focus();
158
- }
159
- };
160
-
161
- onInputMouseUp: (SyntheticMouseEvent<> | SyntheticTouchEvent<>) => void = (
162
- e,
163
- ) => {
164
- if (e.type === "mouseup" && this.justFinishedTouch) {
165
- return;
166
- }
167
-
168
- // NOTE(emily): We do some special handling here of touch events to
169
- // make the "active" effect look better. In particular, when you click
170
- // using touch events, we get a series of events going
171
- // touchstart -> (delay) -> touchend -> mousedown -> mouseup -> click
172
- // In order to make sure that we don't turn the active state off and on
173
- // and off again during the touchend -> mousedown -> mouseup series, we
174
- // set a flag (this.justFinishedTouch) after the touchend, and ignore
175
- // the mousedown and mouseup events. Then, a little while later, we
176
- // turn the flag off. Instead of turning the active state off right at
177
- // the beginning, we wait for a little bit to sync it up better with
178
- // the click event.
179
- if (e.type === "touchend") {
180
- this.justFinishedTouch = true;
181
-
182
- // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
183
- /* eslint-disable no-restricted-syntax */
184
- if (this.justFinishedTimeoutID != null) {
185
- clearTimeout(this.justFinishedTimeoutID);
186
- }
187
- this.justFinishedTimeoutID = setTimeout(() => {
188
- this.setState({isInputActive: false});
189
- this.justFinishedTouch = false;
190
- }, TouchIgnoreTimeout);
191
- /* eslint-enable no-restricted-syntax */
192
- return;
193
- }
194
-
195
- this.setState({isInputActive: false});
196
-
197
- // Simulate Chrome's radio button behavior in all browsers: when the
198
- // mouse goes down or up, the radio button should become focused.
199
- // That way, the newly-selected answer becomes highlighted after click.
200
- if (this.props.apiOptions.satStyling && this._input) {
201
- this._input.focus();
202
- }
203
- };
204
-
205
- onInputMouseOut: () => void = () => {
206
- this.setState({isInputActive: false});
207
- };
208
-
209
133
  inputRef: ($FlowFixMe) => void = (ref) => {
210
134
  this._input = ref;
211
135
  };
@@ -235,7 +159,10 @@ class Choice extends React.Component<$FlowFixMe, State> {
235
159
  );
236
160
  };
237
161
 
238
- renderChoiceIcon: () => React.Node = () => {
162
+ renderChoiceIcon: (args: {|
163
+ isFocused: boolean,
164
+ isPressed: boolean,
165
+ |}) => React.Node = ({isFocused, isPressed}) => {
239
166
  const {radioStyleVersion, primaryProductColor} =
240
167
  this.props.apiOptions.styling;
241
168
  const finalStyles =
@@ -252,8 +179,8 @@ class Choice extends React.Component<$FlowFixMe, State> {
252
179
  pos={this.props.pos}
253
180
  correct={this.props.correct}
254
181
  crossedOut={this.props.crossedOut}
255
- pressed={this.state.isInputActive}
256
- focused={this.state.isInputFocused}
182
+ pressed={isPressed}
183
+ focused={isFocused}
257
184
  checked={this.props.checked}
258
185
  showCorrectness={this.props.showCorrectness}
259
186
  reviewMode={this.props.reviewMode}
@@ -264,26 +191,6 @@ class Choice extends React.Component<$FlowFixMe, State> {
264
191
  );
265
192
  };
266
193
 
267
- _setChecked: (boolean) => void = (willBeChecked) => {
268
- if (willBeChecked) {
269
- // If we're checking a crossed-out option, let's also uncross it.
270
- this._sendChange({checked: true, crossedOut: false});
271
- } else {
272
- this._sendChange({checked: false});
273
- }
274
- };
275
-
276
- // NOTE(mdr): This method expects to be auto-bound. If this component is
277
- // converted to an ES6 class, take care to auto-bind this method!
278
- _setCrossedOut: (boolean) => void = (willBeCrossedOut) => {
279
- if (willBeCrossedOut) {
280
- // If we're crossing out a checked option, let's also uncheck it.
281
- this._sendChange({checked: false, crossedOut: true});
282
- } else {
283
- this._sendChange({crossedOut: false});
284
- }
285
- };
286
-
287
194
  // Call `this.props.onChange` with the given values. Any keys that are not
288
195
  // specified will be filled in with the current value. (For example, if
289
196
  // `checked` is specified but `crossedOut` is not, then `crossedOut` will
@@ -291,15 +198,11 @@ class Choice extends React.Component<$FlowFixMe, State> {
291
198
  //
292
199
  // This enables us to use shorthand inside this component, while
293
200
  // maintaining a consistent API for the parent.
294
- _sendChange: ($FlowFixMe) => void = (newValues) => {
295
- const checked =
296
- newValues.checked != null ? newValues.checked : this.props.checked;
297
-
298
- const crossedOut =
299
- newValues.crossedOut != null
300
- ? newValues.crossedOut
301
- : this.props.crossedOut;
302
-
201
+ _sendChange: ({|checked?: boolean, crossedOut?: boolean|}) => void = (
202
+ newValues,
203
+ ) => {
204
+ const checked = newValues.checked ?? this.props.checked;
205
+ const crossedOut = newValues.crossedOut ?? this.props.crossedOut;
303
206
  this.props.onChange({checked, crossedOut});
304
207
  };
305
208
 
@@ -312,99 +215,6 @@ class Choice extends React.Component<$FlowFixMe, State> {
312
215
 
313
216
  render(): React.Node {
314
217
  const sat = this.props.apiOptions.satStyling;
315
- const isMobile = this.props.apiOptions.isMobile;
316
-
317
- const {radioStyleVersion} = this.props.apiOptions.styling;
318
- const finalStyles =
319
- typeof radioStyleVersion === "undefined"
320
- ? false
321
- : radioStyleVersion === "final";
322
-
323
- const className = classNames(
324
- this.props.className,
325
- "checkbox-label",
326
- css(
327
- styles.label,
328
- isMobile && sharedStyles.disableTextSelection,
329
- !sat && styles.responsiveLabel,
330
- sat && styles.satLabel,
331
- ),
332
- );
333
-
334
- const satRadioMenuContainer = classNames(
335
- css(
336
- this.state.isInputFocused && styles.satDescriptionInputFocused,
337
- this.state.isInputActive && styles.satDescriptionInputActive,
338
- ),
339
- );
340
-
341
- // If we're showing an answer to be incorrect, we render it as
342
- // unchecked, regardless of the `checked` state we've stored.
343
- //
344
- // This is because the existence of a selected radio button makes it
345
- // impossible to tab-navigate to any other choices (standard radio
346
- // behavior), but the selected answer is now disabled and not tabbable,
347
- // which means that non-mouse/non-touch users can't select another
348
- // answer to try again!
349
- //
350
- // (This behavior is only necessary for type "radio", but we also apply
351
- // it for type "checkbox", for consistency.)
352
- const showingIncorrectness =
353
- this.props.showCorrectness && !this.props.correct;
354
- const inputIsChecked = this.props.checked && !showingIncorrectness;
355
-
356
- // There's two different input components we could use (the builtin
357
- // input component, or the ToggleableRadioButton component). These are
358
- // the props that we will pass to either.
359
- const commonInputProps = {
360
- type: this.props.type,
361
- name: this.props.groupName,
362
- id: `${this.props.groupName}-choice-${this.props.pos}`,
363
- checked: inputIsChecked,
364
- disabled: this.props.disabled,
365
- onFocus: this.onInputFocus,
366
- onBlur: this.onInputBlur,
367
- className: css(
368
- // intermediate styles are not different for radio and
369
- // checkbox, and have a separate active state.
370
- !finalStyles && sharedStyles.perseusInteractive,
371
- !finalStyles && styles.input,
372
- !finalStyles && sharedStyles.responsiveInput,
373
- !finalStyles && !sat && sharedStyles.responsiveRadioInput,
374
- !finalStyles &&
375
- !sat &&
376
- this.state.isInputActive &&
377
- sharedStyles.responsiveRadioInputActive,
378
- finalStyles && sharedStyles.perseusSrOnly,
379
- sat && sharedStyles.perseusSrOnly,
380
- sat && this.props.reviewMode && styles.satReviewInput,
381
- ),
382
- };
383
-
384
- let input = null;
385
- if (this.props.type === "radio") {
386
- // This is a special radio button that allows a user to deselect
387
- // it by merely clicking/selecting it again.
388
- input = (
389
- <ToggleableRadioButton
390
- onChecked={(willBeChecked) =>
391
- this._setChecked(willBeChecked)
392
- }
393
- goToPrevChoice={this.props.goToPrevChoice}
394
- goToNextChoice={this.props.goToNextChoice}
395
- inputRef={this.inputRef}
396
- {...commonInputProps}
397
- />
398
- );
399
- } else {
400
- input = (
401
- <input
402
- onChange={(event) => this._setChecked(event.target.checked)}
403
- ref={this.inputRef}
404
- {...commonInputProps}
405
- />
406
- );
407
- }
408
218
 
409
219
  const {reviewMode, correct, checked} = this.props;
410
220
  // HACK: while most of the styling for rendering SAT items is handled
@@ -428,25 +238,6 @@ class Choice extends React.Component<$FlowFixMe, State> {
428
238
  ),
429
239
  );
430
240
 
431
- const checkboxContentClassName = classNames(
432
- "checkbox",
433
- css(
434
- sharedStyles.perseusInteractive,
435
- !sat && styles.choiceIconWrapper,
436
- sat && styles.satCheckboxOptionContent,
437
- ),
438
- );
439
-
440
- const checkboxAndOptionClassName = classNames(
441
- "checkbox-and-option",
442
- css(
443
- !sat && styles.intermediateResponsiveCheckbox,
444
- !sat &&
445
- reviewMode &&
446
- styles.intermediateResponsiveCheckboxReview,
447
- ),
448
- );
449
-
450
241
  const rationaleClassName = classNames(
451
242
  "perseus-radio-rationale-content",
452
243
  css(
@@ -456,97 +247,163 @@ class Choice extends React.Component<$FlowFixMe, State> {
456
247
  ),
457
248
  );
458
249
 
459
- // In edit mode, we must allow selection of the contentEditable
460
- // element inside, therefore we cannot use a label, which makes
461
- // selection of anything inside automatically select the input
462
- // element instead
463
- const LabelOrDiv = this.props.editMode ? "div" : "label";
464
-
465
- // We only show cross-out in certain products, and
466
- // only when you're still _answering_ the question. We also show this
467
- // for the SAT product area.
468
- const crossOutEnabled = this.props.apiOptions.crossOutEnabled;
469
- const showCrossOutMenu = crossOutEnabled && !this.props.showCorrectness;
470
-
471
250
  // We want to show the choices as dimmed out when the choices are
472
251
  // disabled. However, we don't want to do this in the SAT product and
473
252
  // we also don't want to do this when we're in review mode in the
474
253
  // content library.
475
254
  const showDimmed =
476
255
  (!sat && !reviewMode && this.props.apiOptions.readOnly) ||
477
- (showCrossOutMenu && this.props.crossedOut);
256
+ this.props.crossedOut;
478
257
 
479
258
  return (
480
- <div className={sat ? satRadioMenuContainer : undefined}>
481
- <CrossOutMenuWrapper
482
- enabled={showCrossOutMenu}
483
- pos={this.props.pos}
484
- primaryProductColor={
485
- this.props.apiOptions.styling.primaryProductColor
486
- }
487
- crossedOut={this.props.crossedOut}
488
- onCrossedOutChange={this._setCrossedOut}
489
- isSatProduct={sat}
259
+ <div
260
+ style={{
261
+ dispay: "flex",
262
+ flexDirection: "column",
263
+ }}
264
+ >
265
+ <div
266
+ style={{
267
+ display: "flex",
268
+ flexDirection: "row",
269
+ width: "100%",
270
+ opacity: showDimmed ? 0.5 : 1.0,
271
+ }}
490
272
  >
491
- <LabelOrDiv
492
- htmlFor={
493
- !this.props.editMode
494
- ? commonInputProps.id
495
- : undefined
273
+ <Clickable
274
+ onClick={() => {
275
+ // If we're checking a crossed-out option, let's
276
+ // also uncross it.
277
+ this._sendChange({
278
+ checked: true,
279
+ crossedOut: false,
280
+ });
281
+ }}
282
+ className={descriptionClassName}
283
+ disabled={
284
+ this.props.disabled ||
285
+ this.props.apiOptions.staticRender ||
286
+ this.props.apiOptions.readOnly
496
287
  }
497
- className={className}
498
- style={{opacity: showDimmed ? 0.5 : 1.0}}
288
+ aria-label={`Select Choice ${getChoiceLetter(
289
+ this.props.pos,
290
+ )}`}
291
+ aria-checked={this.props.checked}
292
+ role={"checkbox"}
293
+ style={{flex: 1}}
499
294
  >
500
- <div
501
- className={descriptionClassName}
502
- onMouseDown={this.onInputMouseDown}
503
- onMouseUp={this.onInputMouseUp}
504
- onMouseOut={this.onInputMouseOut}
505
- onTouchStart={this.onInputMouseDown}
506
- onTouchEnd={this.onInputMouseUp}
507
- >
508
- <div className={checkboxAndOptionClassName}>
509
- <span className={checkboxContentClassName}>
510
- {input}
511
- {this.renderChoiceIcon()}
295
+ {({hovered, focused, pressed}) => (
296
+ <div
297
+ style={{
298
+ display: "flex",
299
+ flexDirection: "row",
300
+ justifyContent: "center",
301
+ alignContent: "center",
302
+ paddingTop: Spacing.xSmall_8,
303
+ paddingBottom: Spacing.xSmall_8,
304
+ }}
305
+ >
306
+ <span>
307
+ {this.renderChoiceIcon({
308
+ isFocused: focused,
309
+ isPressed: pressed,
310
+ })}
512
311
  </span>
513
- {/* A pseudo-label. <label> is slightly broken on iOS,
514
- so this works around that. Unfortunately, it is
515
- simplest to just work around that everywhere. */}
516
312
  <span
517
- className={classNames(
518
- ClassNames.RADIO.OPTION_CONTENT,
519
- ClassNames.INTERACTIVE,
520
- css(
521
- sat && styles.satRadioOptionContent,
522
- sat &&
523
- reviewMode &&
524
- styles.satReviewRadioOptionContent,
525
- ),
526
- )}
527
- style={{cursor: "default"}}
313
+ style={{
314
+ paddingLeft: Spacing.small_12,
315
+ textAlign: "left",
316
+ flex: 1,
317
+ paddingTop: 4,
318
+ }}
528
319
  >
529
- <div
530
- className={css(
531
- styles.optionStatusContainer,
532
- )}
533
- >
534
- {this.renderOptionStatus()}
535
- </div>
320
+ <div>{this.renderOptionStatus()}</div>
536
321
  <div>{this.props.content}</div>
537
322
  </span>
538
323
  </div>
539
- {this.props.showRationale && (
540
- <div
541
- className={rationaleClassName}
542
- data-test-id={`perseus-radio-rationale-content-${this.props.pos}`}
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
+ }
543
385
  >
544
- {this.props.rationale}
545
- </div>
386
+ {({hovered, focused, pressed}) => (
387
+ <Icon
388
+ icon={ellipsisHorizontalIcon}
389
+ size={3}
390
+ color={Color.offBlack64}
391
+ />
392
+ )}
393
+ </Clickable>
546
394
  )}
547
- </div>
548
- </LabelOrDiv>
549
- </CrossOutMenuWrapper>
395
+ </Popover>
396
+ )}
397
+ </div>
398
+
399
+ {this.props.showRationale && (
400
+ <div
401
+ className={rationaleClassName}
402
+ data-test-id={`perseus-radio-rationale-content-${this.props.pos}`}
403
+ >
404
+ {this.props.rationale}
405
+ </div>
406
+ )}
550
407
  </div>
551
408
  );
552
409
  }
@@ -567,16 +424,6 @@ const styles = StyleSheet.create({
567
424
  padding: "17px 14px",
568
425
  },
569
426
 
570
- satDescriptionInputFocused: {
571
- ...focusedStyleMixin,
572
- display: "block",
573
- },
574
-
575
- satDescriptionInputActive: {
576
- ...focusedStyleMixin,
577
- backgroundColor: styleConstants.satActiveBackgroundColor,
578
- },
579
-
580
427
  satDescriptionCorrect: {
581
428
  color: styleConstants.satCorrectColor,
582
429
  },
@@ -590,53 +437,6 @@ const styles = StyleSheet.create({
590
437
  backgroundColor: styleConstants.satIncorrectBackgroundColor,
591
438
  },
592
439
 
593
- input: {
594
- display: "inline-block",
595
- width: 20,
596
- margin: 3,
597
- marginLeft: -20,
598
- marginRight: 0,
599
- float: "none",
600
- },
601
-
602
- satReviewInput: {
603
- pointerEvents: "none",
604
- },
605
-
606
- satRadioOptionContent: {
607
- userSelect: "text",
608
- display: "block",
609
- marginLeft: 45,
610
- // Overriding here, not sure why typically set
611
- // to "cursor: default" in js
612
- cursor: "inherit",
613
- },
614
-
615
- satReviewRadioOptionContent: {
616
- fontWeight: "bold",
617
- },
618
-
619
- satCheckboxOptionContent: {
620
- position: "absolute",
621
- display: "block",
622
- top: "50%",
623
- margin: "-16px 0 0 0",
624
- width: "auto",
625
- },
626
-
627
- choiceIconWrapper: {
628
- display: "flex",
629
- marginRight: 12,
630
-
631
- // NOTE(mdr): Without this style, the bubbles shrink on iOS
632
- // when answer text gets long.
633
- flexShrink: 0,
634
- },
635
-
636
- optionStatusContainer: {
637
- display: "block",
638
- },
639
-
640
440
  rationale: {
641
441
  display: "block",
642
442
  },
@@ -655,38 +455,6 @@ const styles = StyleSheet.create({
655
455
  marginTop: 13,
656
456
  marginLeft: 45,
657
457
  },
658
-
659
- label: {
660
- display: "block",
661
- transition: "opacity 0.2s ease-out",
662
- },
663
-
664
- responsiveLabel: {
665
- WebkitTapHighlightColor: "transparent",
666
- display: "flex",
667
-
668
- // Ensure that all items consume the full height of the choice.
669
- // That way, the cross-out menu click target is full-height,
670
- // which makes it easier to click!
671
- alignItems: "stretch",
672
- },
673
-
674
- satLabel: {
675
- cursor: "pointer",
676
- },
677
-
678
- intermediateResponsiveCheckbox: {
679
- display: "flex",
680
- alignItems: "center",
681
-
682
- padding: intermediateCheckboxPadding,
683
- [mediaQueries.smOrSmaller]: {
684
- padding: intermediateCheckboxPaddingPhone,
685
- },
686
- },
687
- intermediateResponsiveCheckboxReview: {
688
- alignItems: "flex-start",
689
- },
690
458
  });
691
459
 
692
460
  export default Choice;