@khanacademy/perseus-editor 0.0.1 → 0.1.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.
Files changed (127) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/es/index.css +1020 -0
  4. package/dist/es/index.js +9 -1
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +1020 -0
  7. package/dist/index.js +9 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +6 -1
  10. package/package.json +16 -4
  11. package/src/README.md +7 -0
  12. package/src/__stories__/editor.stories.js +33 -0
  13. package/src/__stories__/section-control-button.stories.jsx +27 -0
  14. package/src/__testdata__/input-number_testdata.js +28 -0
  15. package/src/__tests__/editor_test.jsx +123 -0
  16. package/src/__tests__/i18n_test.jsx +585 -0
  17. package/src/__tests__/traversal_test.jsx +378 -0
  18. package/src/all-editors.js +85 -0
  19. package/src/all-editors_test.js +17 -0
  20. package/src/all-widgets.js +10 -0
  21. package/src/article-editor.jsx +437 -0
  22. package/src/components/__stories__/blur-input.stories.jsx +24 -0
  23. package/src/components/blur-input.jsx +62 -0
  24. package/src/components/checkbox.jsx +194 -0
  25. package/src/components/device-framer.jsx +91 -0
  26. package/src/components/drag-target.jsx +88 -0
  27. package/src/components/dropdown-option.jsx +318 -0
  28. package/src/components/form-wrapped-text-field.jsx +248 -0
  29. package/src/components/graph-settings.jsx +631 -0
  30. package/src/components/hover-behavior.jsx +178 -0
  31. package/src/components/json-editor.jsx +121 -0
  32. package/src/components/link.jsx +173 -0
  33. package/src/components/simple-button.jsx +86 -0
  34. package/src/components/sortable.jsx +186 -0
  35. package/src/components/util.js +39 -0
  36. package/src/components/viewport-resizer.jsx +73 -0
  37. package/src/diffs/__stories__/perseus-diff-wrapper.jsx +8 -0
  38. package/src/diffs/__stories__/structured-item-diff.stories.jsx +855 -0
  39. package/src/diffs/__stories__/tags-diff.stories.jsx +38 -0
  40. package/src/diffs/__stories__/text-diff.stories.jsx +37 -0
  41. package/src/diffs/__tests__/split-diff_test.js +34 -0
  42. package/src/diffs/__tests__/string-array-diff_test.js +74 -0
  43. package/src/diffs/__tests__/structured-item-diff_test.js +432 -0
  44. package/src/diffs/__tests__/widget-diff-performer_test.js +44 -0
  45. package/src/diffs/article-diff.jsx +70 -0
  46. package/src/diffs/item-diff.jsx +77 -0
  47. package/src/diffs/renderer-diff.jsx +143 -0
  48. package/src/diffs/split-diff.jsx +30 -0
  49. package/src/diffs/string-array-diff.jsx +75 -0
  50. package/src/diffs/structured-item-diff.jsx +269 -0
  51. package/src/diffs/tags-diff.jsx +50 -0
  52. package/src/diffs/text-diff.jsx +194 -0
  53. package/src/diffs/widget-diff-performer.jsx +74 -0
  54. package/src/diffs/widget-diff.jsx +291 -0
  55. package/src/editor-page.jsx +351 -0
  56. package/src/editor.jsx +1364 -0
  57. package/src/hint-editor.jsx +447 -0
  58. package/src/i18n.jsx +139 -0
  59. package/src/iframe-content-renderer.jsx +201 -0
  60. package/src/index.js +27 -8
  61. package/src/item-editor.jsx +159 -0
  62. package/src/item-extras-editor.jsx +150 -0
  63. package/src/katex-error-view.jsx +87 -0
  64. package/src/multirenderer-editor.jsx +1074 -0
  65. package/src/section-control-button.jsx +53 -0
  66. package/src/stateful-article-editor.jsx +176 -0
  67. package/src/stateful-editor-page.jsx +90 -0
  68. package/src/styles/perseus-editor.less +1339 -0
  69. package/src/testing-widgets.js +12 -0
  70. package/src/util/register-all-widgets-and-editors-for-testing.js +15 -0
  71. package/src/util/register-all-widgets-and-editors-for-testing_test.js +18 -0
  72. package/src/widgets/__stories__/label-image-editor.stories.jsx +97 -0
  73. package/src/widgets/categorizer-editor.jsx +103 -0
  74. package/src/widgets/cs-program-editor.jsx +258 -0
  75. package/src/widgets/definition-editor.jsx +75 -0
  76. package/src/widgets/dropdown-editor.jsx +201 -0
  77. package/src/widgets/example-graphie-widget-editor.jsx +70 -0
  78. package/src/widgets/example-graphie-widget.jsx +175 -0
  79. package/src/widgets/example-widget-editor.jsx +60 -0
  80. package/src/widgets/example-widget.jsx +153 -0
  81. package/src/widgets/explanation-editor.jsx +89 -0
  82. package/src/widgets/expression-editor.jsx +560 -0
  83. package/src/widgets/graded-group-editor.jsx +162 -0
  84. package/src/widgets/graded-group-set-editor.jsx +101 -0
  85. package/src/widgets/grapher-editor.jsx +154 -0
  86. package/src/widgets/group-editor.jsx +83 -0
  87. package/src/widgets/iframe-editor.jsx +200 -0
  88. package/src/widgets/image-editor.jsx +326 -0
  89. package/src/widgets/input-number-editor.jsx +269 -0
  90. package/src/widgets/interaction/constraint-editor.jsx +147 -0
  91. package/src/widgets/interaction-editor.jsx +1389 -0
  92. package/src/widgets/interactive-graph-editor.jsx +322 -0
  93. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +81 -0
  94. package/src/widgets/label-image/__stories__/behavior.stories.jsx +32 -0
  95. package/src/widgets/label-image/__stories__/marker.stories.jsx +76 -0
  96. package/src/widgets/label-image/__stories__/question-markers.stories.jsx +106 -0
  97. package/src/widgets/label-image/__stories__/select-image.stories.jsx +60 -0
  98. package/src/widgets/label-image/answer-choices.jsx +231 -0
  99. package/src/widgets/label-image/behavior.jsx +92 -0
  100. package/src/widgets/label-image/marker.jsx +292 -0
  101. package/src/widgets/label-image/question-markers.jsx +181 -0
  102. package/src/widgets/label-image/select-image.jsx +86 -0
  103. package/src/widgets/label-image-editor.jsx +257 -0
  104. package/src/widgets/lights-puzzle-editor.jsx +267 -0
  105. package/src/widgets/matcher-editor.jsx +147 -0
  106. package/src/widgets/matrix-editor.jsx +155 -0
  107. package/src/widgets/measurer-editor.jsx +245 -0
  108. package/src/widgets/molecule-editor.jsx +63 -0
  109. package/src/widgets/number-line-editor.jsx +498 -0
  110. package/src/widgets/numeric-input-editor.jsx +554 -0
  111. package/src/widgets/orderer-editor.jsx +166 -0
  112. package/src/widgets/passage-editor.jsx +115 -0
  113. package/src/widgets/passage-ref-editor.jsx +77 -0
  114. package/src/widgets/passage-ref-target-editor.jsx +47 -0
  115. package/src/widgets/plotter-editor.jsx +537 -0
  116. package/src/widgets/radio/editor.jsx +398 -0
  117. package/src/widgets/reaction-diagram-editor.jsx +127 -0
  118. package/src/widgets/sequence-editor.jsx +205 -0
  119. package/src/widgets/simple-markdown-tester-editor.jsx +84 -0
  120. package/src/widgets/simple-markdown-tester.jsx +91 -0
  121. package/src/widgets/simulator-editor.jsx +144 -0
  122. package/src/widgets/sorter-editor.jsx +100 -0
  123. package/src/widgets/table-editor.jsx +164 -0
  124. package/src/widgets/transformer-editor.jsx +1006 -0
  125. package/src/widgets/unit-editor.jsx +293 -0
  126. package/src/widgets/video-editor.jsx +74 -0
  127. package/src/__tests__/index.test.js +0 -15
@@ -0,0 +1,231 @@
1
+ // @flow
2
+ /**
3
+ * Controlled list of answer choices, handles adding and removing answers.
4
+ */
5
+
6
+ import {components, globalStyles} from "@khanacademy/perseus";
7
+ import {StyleSheet, css} from "aphrodite";
8
+ import * as React from "react";
9
+
10
+ import FormWrappedTextField from "../../components/form-wrapped-text-field.jsx";
11
+ import Link from "../../components/link.jsx";
12
+
13
+ const {colors, typography} = globalStyles;
14
+ const {Icon} = components;
15
+
16
+ type AddAnswerProps = {
17
+ // Callback to add new answer choice.
18
+ onClick: () => void,
19
+ ...
20
+ };
21
+
22
+ type AnswerProps = {
23
+ // The answer string, can be plain text or a KaTeX expression.
24
+ answer: string,
25
+ // Callback for when an answer is changed.
26
+ onChange: (answer: string) => void,
27
+ // Callback to remove answer from list of choices.
28
+ onRemove: () => void,
29
+ ...
30
+ };
31
+
32
+ type AnswerChoicesProps = {
33
+ // The list of possible answers in a specific order.
34
+ choices: $ReadOnlyArray<string>,
35
+ // Callback for when answers change.
36
+ onChange: (choices: $ReadOnlyArray<string>) => void,
37
+ ...
38
+ };
39
+
40
+ const addIcon = {
41
+ path: "M11 11V7a1 1 0 0 1 2 0v4h4a1 1 0 0 1 0 2h-4v4a1 1 0 0 1-2 0v-4H7a1 1 0 0 1 0-2h4zm1 13C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12zm0-2c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z",
42
+ width: 24,
43
+ height: 24,
44
+ };
45
+
46
+ const removeIcon = {
47
+ path: "M12 24C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12zm0-2c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm-5-9a1 1 0 0 1 0-2h10a1 1 0 0 1 0 2H7z",
48
+ width: 24,
49
+ height: 24,
50
+ };
51
+
52
+ const DraggableGripIcon = () => (
53
+ <svg width="16" height="16" viewBox="0 0 16 16">
54
+ <filter
55
+ id="draggable-grip-shadow"
56
+ width="112.5%"
57
+ height="116.7%"
58
+ x="-6.2%"
59
+ y="-8.3%"
60
+ filterUnits="objectBoundingBox"
61
+ >
62
+ <feGaussianBlur
63
+ in="SourceAlpha"
64
+ stdDeviation=".5"
65
+ result="shadowBlurInner"
66
+ />
67
+
68
+ <feOffset in="shadowBlurInner" dy="1" result="shadowOffsetInner" />
69
+
70
+ <feComposite
71
+ in="shadowOffsetInner"
72
+ in2="SourceAlpha"
73
+ k2="-1"
74
+ k3="1"
75
+ operator="arithmetic"
76
+ result="shadowInnerInner"
77
+ />
78
+
79
+ <feColorMatrix
80
+ in="shadowInnerInner"
81
+ values="0 0 0 0 0.129411765 0 0 0 0 0.141176471 0 0 0 0 0.17254902 0 0 0 0.2 0"
82
+ />
83
+ </filter>
84
+
85
+ <path
86
+ d="M1 4a1 1 0 1 1 0-2h14a1 1 0 0 1 0 2H1zm0 10a1 1 0 0 1 0-2h14a1 1 0 0 1 0 2H1zm0-5a1 1 0 1 1 0-2h14a1 1 0 0 1 0 2H1z"
87
+ fill={colors.gray17}
88
+ filter="url(#draggable-grip-shadow)"
89
+ />
90
+ </svg>
91
+ );
92
+
93
+ /**
94
+ * A button link to add a new answer.
95
+ */
96
+ const AddAnswer = ({onClick}: AddAnswerProps) => (
97
+ <Link
98
+ className={css(styles.addAnswer, editorStyles.addAnswer)}
99
+ onClick={onClick}
100
+ >
101
+ <Icon icon={addIcon} size={24} />
102
+ <div className={css(styles.spacer)} />
103
+ Add an answer choice
104
+ </Link>
105
+ );
106
+
107
+ /**
108
+ * An answer item in the choices list.
109
+ *
110
+ * TODO(michaelpolyak): Implement answer reordering, CP-117
111
+ */
112
+ const Answer = ({answer, onChange, onRemove}: AnswerProps) => (
113
+ <li className={css(styles.answer)}>
114
+ <Link onClick={onRemove}>
115
+ <Icon icon={removeIcon} size={24} color="#D92916" />
116
+ </Link>
117
+
118
+ <div className={css(styles.spacer)} />
119
+
120
+ <FormWrappedTextField
121
+ grow={1}
122
+ onChange={(e) => onChange(e.target.value)}
123
+ value={answer}
124
+ />
125
+
126
+ <div className={css(styles.spacer)} />
127
+
128
+ <Link
129
+ style={[styles.disabled]}
130
+ title="Answer reordering is not implemented."
131
+ >
132
+ <DraggableGripIcon />
133
+ </Link>
134
+ </li>
135
+ );
136
+
137
+ /**
138
+ * The list of choices, handles adding, removing and reording of answers.
139
+ */
140
+ const AnswerChoices = ({
141
+ choices,
142
+ onChange,
143
+ }: AnswerChoicesProps): React.Element<"div"> => (
144
+ <div>
145
+ <div className={css(styles.title)}>Answer Choices</div>
146
+
147
+ <ul className={css(styles.answers)}>
148
+ {choices.map((answer, index) => (
149
+ <Answer
150
+ answer={answer}
151
+ // TODO(michaelpolyak): When answer reording is implemented,
152
+ // key by index may not re-render correctly, CP-117
153
+ key={index}
154
+ // Update answer for choice.
155
+ onChange={(answer) =>
156
+ onChange([
157
+ ...choices.slice(0, index),
158
+ answer,
159
+ ...choices.slice(index + 1),
160
+ ])
161
+ }
162
+ // Remove answer from choices.
163
+ onRemove={() =>
164
+ onChange([
165
+ ...choices.slice(0, index),
166
+ ...choices.slice(index + 1),
167
+ ])
168
+ }
169
+ />
170
+ ))}
171
+ </ul>
172
+
173
+ <AddAnswer
174
+ // Append a new empty answer to choices.
175
+ onClick={() => onChange([...choices, ""])}
176
+ />
177
+ </div>
178
+ );
179
+
180
+ const styles = StyleSheet.create({
181
+ title: {
182
+ ...typography.bodyXsmallBold,
183
+
184
+ marginBottom: 6,
185
+
186
+ color: colors.gray17,
187
+ },
188
+
189
+ answers: {
190
+ marginTop: 12,
191
+ marginBottom: 12,
192
+ },
193
+
194
+ answer: {
195
+ display: "flex",
196
+ flexDirection: "row",
197
+ alignItems: "center",
198
+
199
+ ":not(:first-child)": {
200
+ marginTop: 12,
201
+ },
202
+ },
203
+
204
+ addAnswer: {
205
+ ...typography.bodyXsmallBold,
206
+
207
+ display: "flex",
208
+ flexDirection: "row",
209
+ alignItems: "center",
210
+
211
+ color: "#1865f2",
212
+ },
213
+
214
+ spacer: {
215
+ width: 16,
216
+ },
217
+
218
+ disabled: {
219
+ cursor: "not-allowed",
220
+ },
221
+ });
222
+
223
+ const editorStyles = StyleSheet.create({
224
+ addAnswer: {
225
+ ":link": {
226
+ color: "#1865f2",
227
+ },
228
+ },
229
+ });
230
+
231
+ export default AnswerChoices;
@@ -0,0 +1,92 @@
1
+ // @flow
2
+ /**
3
+ * Controlled component to display and change widget options.
4
+ */
5
+
6
+ import {globalStyles} from "@khanacademy/perseus";
7
+ import {StyleSheet, css} from "aphrodite";
8
+ import * as React from "react";
9
+
10
+ import Checkbox from "../../components/checkbox.jsx";
11
+
12
+ const {colors, typography} = globalStyles;
13
+
14
+ type BehaviorProps = {
15
+ // Whether multiple answer choices may be selected for markers.
16
+ multipleAnswers: boolean,
17
+ // Whether to hide answer choices from user instructions.
18
+ hideChoicesFromInstructions: boolean,
19
+ // Callback for when widget options change.
20
+ onChange: (options: {|
21
+ multipleAnswers?: boolean,
22
+ hideChoicesFromInstructions?: boolean,
23
+ |}) => void,
24
+ ...
25
+ };
26
+
27
+ const Behavior = ({
28
+ multipleAnswers,
29
+ hideChoicesFromInstructions,
30
+ onChange,
31
+ }: BehaviorProps): React.Element<"div"> => (
32
+ <div>
33
+ <div className={css(styles.title)}>Behavior</div>
34
+
35
+ <ul>
36
+ <li className={css(styles.option)}>
37
+ <Checkbox
38
+ checked={multipleAnswers}
39
+ onChange={() =>
40
+ onChange({multipleAnswers: !multipleAnswers})
41
+ }
42
+ />
43
+
44
+ <span className={css(styles.label)}>
45
+ Allow multiple answers per marker
46
+ </span>
47
+ </li>
48
+
49
+ <li className={css(styles.option)}>
50
+ <Checkbox
51
+ checked={hideChoicesFromInstructions}
52
+ onChange={() =>
53
+ onChange({
54
+ hideChoicesFromInstructions:
55
+ !hideChoicesFromInstructions,
56
+ })
57
+ }
58
+ />
59
+
60
+ <span className={css(styles.label)}>
61
+ Do not display answer choices in instructions
62
+ </span>
63
+ </li>
64
+ </ul>
65
+ </div>
66
+ );
67
+
68
+ const styles = StyleSheet.create({
69
+ title: {
70
+ ...typography.bodyXsmallBold,
71
+
72
+ marginBottom: 6,
73
+
74
+ color: colors.gray17,
75
+ },
76
+
77
+ option: {
78
+ display: "flex",
79
+
80
+ padding: "6px 0",
81
+ },
82
+
83
+ label: {
84
+ ...typography.smallSubheadingMobile,
85
+
86
+ marginLeft: 16,
87
+
88
+ color: colors.gray17,
89
+ },
90
+ });
91
+
92
+ export default Behavior;
@@ -0,0 +1,292 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ /**
4
+ * Controlled editor image marker on the question image.
5
+ *
6
+ * Allows to select which label choices are the answers for each marker, using
7
+ * the dropdown component.
8
+ */
9
+
10
+ import {globalConstants, globalStyles} from "@khanacademy/perseus";
11
+ import {StyleSheet, css} from "aphrodite";
12
+ import * as React from "react";
13
+
14
+ import Option, {OptionGroup} from "../../components/dropdown-option.jsx";
15
+ import FormWrappedTextField from "../../components/form-wrapped-text-field.jsx";
16
+
17
+ import type {MarkerType} from "@khanacademy/perseus";
18
+
19
+ const {colors, borderRadius} = globalStyles;
20
+
21
+ type MarkerProps = {|
22
+ ...MarkerType,
23
+
24
+ // The list of possible answer choices.
25
+ choices: $ReadOnlyArray<string>,
26
+
27
+ // Callback for when any of the marker props are changed.
28
+ onChange: (marker: MarkerType) => void,
29
+
30
+ // Callback to remove marker from the question image.
31
+ onRemove: () => void,
32
+ |};
33
+
34
+ type MarkerState = {|
35
+ // Whether answer choices dropdown is shown, controlled by the user clicking
36
+ // on the marker icon.
37
+ showDropdown: boolean,
38
+ |};
39
+
40
+ export default class Marker extends React.Component<MarkerProps, MarkerState> {
41
+ _marker: ?HTMLElement;
42
+
43
+ constructor(props: MarkerProps) {
44
+ super(props);
45
+
46
+ this.state = {
47
+ showDropdown: false,
48
+ };
49
+ }
50
+
51
+ componentDidMount() {
52
+ document.addEventListener("click", this.handleClick, true);
53
+ }
54
+
55
+ UNSAFE_componentWillReceiveProps(nextProps: MarkerProps) {
56
+ const {answers} = this.props;
57
+
58
+ // Exclude those answers that are no longer present in choices.
59
+ const filteredAnswers = answers.filter((answer) =>
60
+ nextProps.choices.includes(answer),
61
+ );
62
+
63
+ if (JSON.stringify(answers) !== JSON.stringify(filteredAnswers)) {
64
+ // Update marker on the next frame when these props take affect.
65
+ // TODO(jeff, CP-3128): Use Wonder Blocks Timing API
66
+ // eslint-disable-next-line no-restricted-syntax
67
+ setTimeout(() => this.updateMarker({answers: filteredAnswers}));
68
+ }
69
+ }
70
+
71
+ componentWillUnmount() {
72
+ document.removeEventListener("click", this.handleClick, true);
73
+ }
74
+
75
+ openDropdown() {
76
+ this.setState({showDropdown: true});
77
+ }
78
+
79
+ updateMarker(props: any) {
80
+ const {answers, label, onChange, x, y} = this.props;
81
+
82
+ onChange({
83
+ answers,
84
+ label,
85
+ x,
86
+ y,
87
+ // Overwrite with updated prop(s).
88
+ ...props,
89
+ });
90
+ }
91
+
92
+ handleClick: (e: MouseEvent) => void = (e: MouseEvent) => {
93
+ const {showDropdown} = this.state;
94
+
95
+ if (this._marker === e.target) {
96
+ this.setState({showDropdown: !showDropdown});
97
+ } else if (showDropdown) {
98
+ // Close dropdown if click event was registered anywhere outside it.
99
+ if (this._marker && !this._marker.contains((e.target: any))) {
100
+ // Ensure other listeners are not triggered on click event that
101
+ // closes the dropdown. A specific case this addresses is when
102
+ // user clicks on question image to close dropdown, a new marker
103
+ // will be created, this is bad for usability.
104
+ e.stopPropagation();
105
+
106
+ this.setState({showDropdown: false});
107
+ }
108
+ }
109
+ };
110
+
111
+ handleLabelChange: (e: SyntheticInputEvent<>) => void = (e) => {
112
+ this.updateMarker({label: e.target.value});
113
+ };
114
+
115
+ handleSelectAnswer: (toggleAnswer: string) => void = (
116
+ toggleAnswer: string,
117
+ ) => {
118
+ let {answers} = this.props;
119
+
120
+ if (answers.includes(toggleAnswer)) {
121
+ answers = answers.filter((answer) => answer !== toggleAnswer);
122
+ } else {
123
+ answers = [...answers, toggleAnswer];
124
+ }
125
+
126
+ this.updateMarker({answers});
127
+ };
128
+
129
+ render(): React.Element<"div"> {
130
+ const {answers, choices, label, onRemove, x, y} = this.props;
131
+
132
+ const {showDropdown} = this.state;
133
+
134
+ return (
135
+ <div
136
+ className={css(
137
+ styles.marker,
138
+ answers.length > 0 && styles.markerWithAnswers,
139
+ showDropdown && styles.markerSelected,
140
+ )}
141
+ ref={(node) => (this._marker = node)}
142
+ style={{
143
+ left: `${x}%`,
144
+ top: `${y}%`,
145
+ }}
146
+ title={
147
+ "Click to select marker answers or to delete marker. " +
148
+ "Repositioning marker is not implemented."
149
+ }
150
+ >
151
+ {showDropdown && (
152
+ <div>
153
+ <div
154
+ className={css(
155
+ styles.dropdownBody,
156
+ styles.dropdownPositionWithArrow,
157
+ )}
158
+ >
159
+ <Option value="" onClick={() => onRemove()}>
160
+ Delete marker
161
+ </Option>
162
+
163
+ <hr className={css(styles.dividerHorizontal)} />
164
+
165
+ <OptionGroup
166
+ onSelected={this.handleSelectAnswer}
167
+ // TODO(WB-1096): make selectedValues immutable in wonder-blocks
168
+ // $FlowFixMe[incompatible-type]
169
+ selectedValues={answers}
170
+ >
171
+ {choices.map((choice) => (
172
+ <Option key={choice} value={choice}>
173
+ {choice}
174
+ </Option>
175
+ ))}
176
+ </OptionGroup>
177
+
178
+ <div className={css(styles.labelContainer)}>
179
+ <FormWrappedTextField
180
+ placeholder="ARIA label (for screen readers)"
181
+ onChange={this.handleLabelChange}
182
+ value={label}
183
+ width="100%"
184
+ />
185
+ </div>
186
+ </div>
187
+ </div>
188
+ )}
189
+ </div>
190
+ );
191
+ }
192
+ }
193
+
194
+ const styles = StyleSheet.create({
195
+ marker: {
196
+ position: "absolute",
197
+
198
+ boxSizing: "content-box",
199
+
200
+ width: 16,
201
+ height: 16,
202
+ marginLeft: -8,
203
+ marginTop: -8,
204
+
205
+ cursor: "pointer",
206
+
207
+ background:
208
+ "linear-gradient(to bottom, rgba(33, 36, 44, 0.2), rgba(33, 36, 44, 0.5))",
209
+
210
+ border: "solid 2px #ffffff",
211
+ borderRadius: 16,
212
+
213
+ boxShadow: "0 2px 10px 0 rgba(33, 36, 44, 0.1)",
214
+ },
215
+
216
+ markerSelected: {
217
+ width: 28,
218
+ height: 28,
219
+ marginLeft: -12,
220
+ marginTop: -12,
221
+
222
+ border: "none",
223
+ borderRadius: 28,
224
+
225
+ // Render selected marker border as inset.
226
+ "::before": {
227
+ content: "''",
228
+ display: "block",
229
+
230
+ width: 20,
231
+ height: 20,
232
+ marginLeft: 2,
233
+ marginTop: 2,
234
+
235
+ border: "solid 2px #ffffff",
236
+ borderRadius: 20,
237
+ },
238
+ },
239
+
240
+ markerWithAnswers: {
241
+ background: "#1865f2",
242
+ },
243
+
244
+ dropdownPositionWithArrow: {
245
+ // Position dropdown to the top right of the marker.
246
+ left: 46,
247
+ bottom: -12,
248
+
249
+ // With an arrow pointing left towards the marker.
250
+ "::before": {
251
+ content: "''",
252
+ display: "block",
253
+ position: "absolute",
254
+
255
+ width: 0,
256
+ height: 0,
257
+ left: -16,
258
+ bottom: 8,
259
+
260
+ borderRight: `solid 16px ${colors.gray98}`,
261
+ borderTop: "solid 16px transparent",
262
+ borderBottom: "solid 16px transparent",
263
+ },
264
+ },
265
+
266
+ labelContainer: {
267
+ padding: 4,
268
+ },
269
+
270
+ dividerHorizontal: {
271
+ height: 0,
272
+ margin: 0,
273
+
274
+ border: `solid ${colors.gray85}`,
275
+ borderWidth: "0 0 1px",
276
+
277
+ boxShadow: "none",
278
+ },
279
+
280
+ // TODO (josh): This should probably be its own component, rather than
281
+ // sharing styles this way.
282
+ dropdownBody: {
283
+ position: "absolute",
284
+ border: "solid 1px rgba(0, 0, 0, 0.1)",
285
+ zIndex: globalConstants.zindexDropdown,
286
+ color: colors.gray17,
287
+ backgroundColor: colors.gray98,
288
+ borderRadius: borderRadius,
289
+ maxHeight: 320,
290
+ cursor: "pointer",
291
+ },
292
+ });