@pie-lib/mask-markup 1.13.46 → 1.14.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -56
- package/NEXT.CHANGELOG.json +1 -0
- package/lib/choices/choice.js +80 -17
- package/lib/choices/choice.js.map +1 -1
- package/lib/choices/index.js +11 -3
- package/lib/choices/index.js.map +1 -1
- package/lib/components/blank.js +146 -34
- package/lib/components/blank.js.map +1 -1
- package/lib/components/correct-input.js +8 -3
- package/lib/components/correct-input.js.map +1 -1
- package/lib/components/dropdown.js +182 -49
- package/lib/components/dropdown.js.map +1 -1
- package/lib/constructed-response.js +87 -23
- package/lib/constructed-response.js.map +1 -1
- package/lib/customizable.js +48 -0
- package/lib/customizable.js.map +1 -0
- package/lib/drag-in-the-blank.js +34 -8
- package/lib/drag-in-the-blank.js.map +1 -1
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -1
- package/lib/inline-dropdown.js +3 -1
- package/lib/inline-dropdown.js.map +1 -1
- package/lib/mask.js +45 -6
- package/lib/mask.js.map +1 -1
- package/lib/with-mask.js +34 -2
- package/lib/with-mask.js.map +1 -1
- package/package.json +9 -5
- package/src/__tests__/__snapshots__/drag-in-the-blank.test.js.snap +316 -0
- package/src/__tests__/__snapshots__/mask.test.js.snap +55 -0
- package/src/__tests__/__snapshots__/with-mask.test.js.snap +62 -0
- package/src/__tests__/drag-in-the-blank.test.js +71 -0
- package/src/__tests__/index.test.js +39 -0
- package/src/__tests__/mask.test.js +152 -0
- package/src/__tests__/serialization.test.js +54 -0
- package/src/__tests__/utils.js +1 -0
- package/src/__tests__/with-mask.test.js +51 -0
- package/src/choices/__tests__/__snapshots__/index.test.js.snap +209 -0
- package/src/choices/__tests__/index.test.js +62 -0
- package/src/choices/choice.jsx +60 -6
- package/src/choices/index.jsx +2 -2
- package/src/components/__tests__/__snapshots__/blank.test.js.snap +111 -0
- package/src/components/__tests__/__snapshots__/correct-input.test.js.snap +64 -0
- package/src/components/__tests__/__snapshots__/dropdown.test.js.snap +133 -0
- package/src/components/__tests__/__snapshots__/input.test.js.snap +34 -0
- package/src/components/__tests__/blank.test.js +202 -0
- package/src/components/__tests__/correct-input.test.js +49 -0
- package/src/components/__tests__/dropdown.test.js +51 -0
- package/src/components/__tests__/input.test.js +50 -0
- package/src/components/blank.jsx +139 -28
- package/src/components/correct-input.jsx +6 -1
- package/src/components/dropdown.jsx +192 -71
- package/src/constructed-response.jsx +76 -18
- package/src/customizable.jsx +35 -0
- package/src/drag-in-the-blank.jsx +26 -3
- package/src/index.js +10 -1
- package/src/inline-dropdown.jsx +2 -0
- package/src/mask.jsx +30 -5
- package/src/with-mask.jsx +39 -2
- package/README.md +0 -14
package/src/components/blank.jsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import grey from '@material-ui/core/colors/grey';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import ReactDOM from 'react-dom';
|
|
3
4
|
import PropTypes from 'prop-types';
|
|
@@ -8,6 +9,7 @@ import { withStyles } from '@material-ui/core/styles';
|
|
|
8
9
|
import Chip from '@material-ui/core/Chip';
|
|
9
10
|
import classnames from 'classnames';
|
|
10
11
|
import { color } from '@pie-lib/render-ui';
|
|
12
|
+
|
|
11
13
|
const log = debug('pie-lib:mask-markup:blank');
|
|
12
14
|
export const DRAG_TYPE = 'MaskBlank';
|
|
13
15
|
|
|
@@ -17,20 +19,19 @@ const useStyles = withStyles(() => ({
|
|
|
17
19
|
minWidth: '200px',
|
|
18
20
|
touchAction: 'none',
|
|
19
21
|
overflow: 'hidden',
|
|
22
|
+
whiteSpace: 'nowrap', // Prevent line wrapping
|
|
20
23
|
},
|
|
21
24
|
chip: {
|
|
22
25
|
backgroundColor: color.background(),
|
|
23
|
-
border: `
|
|
26
|
+
border: `2px dashed ${color.text()}`,
|
|
24
27
|
color: color.text(),
|
|
25
|
-
minWidth: '90px',
|
|
26
28
|
fontSize: 'inherit',
|
|
27
|
-
minHeight: '32px',
|
|
28
|
-
height: 'auto',
|
|
29
29
|
maxWidth: '374px',
|
|
30
30
|
position: 'relative',
|
|
31
|
+
borderRadius: '3px',
|
|
31
32
|
},
|
|
32
33
|
chipLabel: {
|
|
33
|
-
whiteSpace: '
|
|
34
|
+
whiteSpace: 'normal',
|
|
34
35
|
// Added for touch devices, for image content.
|
|
35
36
|
// This will prevent the context menu from appearing and not allowing other interactions with the image.
|
|
36
37
|
// If interactions with the image in the token will be requested we should handle only the context Menu.
|
|
@@ -39,6 +40,16 @@ const useStyles = withStyles(() => ({
|
|
|
39
40
|
display: 'block',
|
|
40
41
|
padding: '2px 0',
|
|
41
42
|
},
|
|
43
|
+
// Remove default <p> margins to ensure consistent spacing across all wrapped content (p, span, div, math)
|
|
44
|
+
// Padding for top and bottom will instead be controlled by the container for consistent layout
|
|
45
|
+
// Ensures consistent behavior with pie-api-browser, where marginTop is already removed by a Bootstrap stylesheet
|
|
46
|
+
'& p': {
|
|
47
|
+
marginTop: '0',
|
|
48
|
+
marginBottom: '0',
|
|
49
|
+
},
|
|
50
|
+
'& mjx-frac': {
|
|
51
|
+
fontSize: '120% !important',
|
|
52
|
+
},
|
|
42
53
|
},
|
|
43
54
|
hidden: {
|
|
44
55
|
color: 'transparent',
|
|
@@ -59,28 +70,44 @@ const useStyles = withStyles(() => ({
|
|
|
59
70
|
whiteSpace: 'nowrap',
|
|
60
71
|
overflow: 'hidden',
|
|
61
72
|
},
|
|
73
|
+
parentOver: {
|
|
74
|
+
border: `1px solid ${grey[500]}`,
|
|
75
|
+
backgroundColor: `${grey[300]}`,
|
|
76
|
+
},
|
|
62
77
|
}));
|
|
63
78
|
|
|
64
79
|
export class BlankContent extends React.Component {
|
|
65
|
-
static propTypes = {
|
|
66
|
-
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
67
|
-
disabled: PropTypes.bool,
|
|
68
|
-
duplicates: PropTypes.bool,
|
|
69
|
-
choice: PropTypes.object,
|
|
70
|
-
classes: PropTypes.object,
|
|
71
|
-
isOver: PropTypes.bool,
|
|
72
|
-
dragItem: PropTypes.object,
|
|
73
|
-
correct: PropTypes.bool,
|
|
74
|
-
onChange: PropTypes.func,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
80
|
constructor() {
|
|
78
81
|
super();
|
|
79
82
|
this.state = {
|
|
80
83
|
height: 0,
|
|
84
|
+
width: 0,
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
handleImageLoad = () => {
|
|
89
|
+
this.updateDimensions();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
handleElements() {
|
|
93
|
+
const imageElement = this.spanRef?.querySelector('img');
|
|
94
|
+
|
|
95
|
+
if (imageElement) {
|
|
96
|
+
imageElement.onload = this.handleImageLoad;
|
|
97
|
+
} else {
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
this.updateDimensions();
|
|
100
|
+
}, 300);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
componentDidMount() {
|
|
105
|
+
this.handleElements();
|
|
106
|
+
if (this.rootRef) {
|
|
107
|
+
this.rootRef.addEventListener('touchstart', this.handleTouchStart, { passive: false });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
componentDidUpdate(prevProps) {
|
|
85
112
|
renderMath(this.rootRef);
|
|
86
113
|
const { choice: currentChoice } = this.props;
|
|
@@ -90,14 +117,59 @@ export class BlankContent extends React.Component {
|
|
|
90
117
|
if (!currentChoice) {
|
|
91
118
|
this.setState({
|
|
92
119
|
height: 0,
|
|
120
|
+
width: 0,
|
|
93
121
|
});
|
|
94
122
|
return;
|
|
95
123
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
this.handleElements();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
componentWillUnmount() {
|
|
129
|
+
if (this.rootRef) {
|
|
130
|
+
this.rootRef.removeEventListener('touchstart', this.handleTouchStart);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
handleTouchStart = (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
this.touchStartTimer = setTimeout(() => {
|
|
137
|
+
this.startDrag();
|
|
138
|
+
}, 300); // Start drag after 300ms (touch and hold duration)
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
startDrag = () => {
|
|
142
|
+
const { connectDragSource, disabled } = this.props;
|
|
143
|
+
if (!disabled) {
|
|
144
|
+
connectDragSource(this.rootRef);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
updateDimensions() {
|
|
149
|
+
if (this.spanRef && this.rootRef) {
|
|
150
|
+
// Temporarily set rootRef width to 'auto' for natural measurement
|
|
151
|
+
this.rootRef.style.width = 'auto';
|
|
152
|
+
|
|
153
|
+
// Get the natural dimensions of the content
|
|
154
|
+
const width = this.spanRef.offsetWidth || 0;
|
|
155
|
+
const height = this.spanRef.offsetHeight || 0;
|
|
156
|
+
|
|
157
|
+
const widthWithPadding = width + 24; // 12px padding on each side
|
|
158
|
+
const heightWithPadding = height + 24; // 12px padding on top and bottom
|
|
159
|
+
|
|
160
|
+
const responseAreaWidth = parseFloat(this.props.emptyResponseAreaWidth) || 0;
|
|
161
|
+
const responseAreaHeight = parseFloat(this.props.emptyResponseAreaHeight) || 0;
|
|
162
|
+
|
|
163
|
+
const adjustedWidth = widthWithPadding <= responseAreaWidth ? responseAreaWidth : widthWithPadding;
|
|
164
|
+
const adjustedHeight = heightWithPadding <= responseAreaHeight ? responseAreaHeight : heightWithPadding;
|
|
165
|
+
|
|
166
|
+
this.setState((prevState) => ({
|
|
167
|
+
width: adjustedWidth > responseAreaWidth ? adjustedWidth : prevState.width,
|
|
168
|
+
height: adjustedHeight > responseAreaHeight ? adjustedHeight : prevState.height,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
this.rootRef.style.width = `${adjustedWidth}px`;
|
|
172
|
+
this.rootRef.style.height = `${adjustedHeight}px`;
|
|
101
173
|
}
|
|
102
174
|
}
|
|
103
175
|
|
|
@@ -109,6 +181,29 @@ export class BlankContent extends React.Component {
|
|
|
109
181
|
});
|
|
110
182
|
}
|
|
111
183
|
|
|
184
|
+
getRootDimensions() {
|
|
185
|
+
// Handle potential non-numeric values
|
|
186
|
+
const responseAreaWidth = !isNaN(parseFloat(this.props.emptyResponseAreaWidth))
|
|
187
|
+
? parseFloat(this.props.emptyResponseAreaWidth)
|
|
188
|
+
: 0;
|
|
189
|
+
const responseAreaHeight = !isNaN(parseFloat(this.props.emptyResponseAreaHeight))
|
|
190
|
+
? parseFloat(this.props.emptyResponseAreaHeight)
|
|
191
|
+
: 0;
|
|
192
|
+
|
|
193
|
+
const rootStyle = {
|
|
194
|
+
height: this.state.height || responseAreaHeight,
|
|
195
|
+
width: this.state.width || responseAreaWidth,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// add minWidth, minHeight if width and height are not defined
|
|
199
|
+
// minWidth, minHeight will be also in model in the future
|
|
200
|
+
return {
|
|
201
|
+
...rootStyle,
|
|
202
|
+
...(responseAreaWidth ? {} : { minWidth: 90 }),
|
|
203
|
+
...(responseAreaHeight ? {} : { minHeight: 32 }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
112
207
|
render() {
|
|
113
208
|
const { disabled, choice, classes, isOver, dragItem, correct } = this.props;
|
|
114
209
|
const draggedLabel = dragItem && isOver && dragItem.choice.value;
|
|
@@ -158,22 +253,38 @@ export class BlankContent extends React.Component {
|
|
|
158
253
|
)}
|
|
159
254
|
</React.Fragment>
|
|
160
255
|
}
|
|
161
|
-
className={classnames(classes.chip, isOver && classes.over, {
|
|
256
|
+
className={classnames(classes.chip, isOver && classes.over, isOver && classes.parentOver, {
|
|
162
257
|
[classes.correct]: correct !== undefined && correct,
|
|
163
258
|
[classes.incorrect]: correct !== undefined && !correct,
|
|
164
259
|
})}
|
|
165
260
|
variant={disabled ? 'outlined' : undefined}
|
|
166
261
|
style={{
|
|
167
|
-
...
|
|
168
|
-
}}
|
|
169
|
-
classes={{
|
|
170
|
-
label: isOver && classes.over,
|
|
262
|
+
...this.getRootDimensions(),
|
|
171
263
|
}}
|
|
172
264
|
/>
|
|
173
265
|
);
|
|
174
266
|
}
|
|
175
267
|
}
|
|
176
268
|
|
|
269
|
+
BlankContent.defaultProps = {
|
|
270
|
+
emptyResponseAreaWidth: 0,
|
|
271
|
+
emptyResponseAreaHeight: 0,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
BlankContent.propTypes = {
|
|
275
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
276
|
+
disabled: PropTypes.bool,
|
|
277
|
+
duplicates: PropTypes.bool,
|
|
278
|
+
choice: PropTypes.object,
|
|
279
|
+
classes: PropTypes.object,
|
|
280
|
+
isOver: PropTypes.bool,
|
|
281
|
+
dragItem: PropTypes.object,
|
|
282
|
+
correct: PropTypes.bool,
|
|
283
|
+
onChange: PropTypes.func,
|
|
284
|
+
emptyResponseAreaWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
285
|
+
emptyResponseAreaHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
286
|
+
};
|
|
287
|
+
|
|
177
288
|
const StyledBlankContent = useStyles(BlankContent);
|
|
178
289
|
|
|
179
290
|
const connectedBlankContent = useStyles(({ connectDragSource, connectDropTarget, ...props }) => {
|
|
@@ -231,7 +342,7 @@ const tileSource = {
|
|
|
231
342
|
// this will be null if it did not drop
|
|
232
343
|
const dropResult = monitor.getDropResult();
|
|
233
344
|
|
|
234
|
-
if (!dropResult ||
|
|
345
|
+
if (!dropResult || dropResult.dropped) {
|
|
235
346
|
const draggedItem = monitor.getItem();
|
|
236
347
|
|
|
237
348
|
if (draggedItem.fromChoice) {
|
|
@@ -37,6 +37,8 @@ export default withStyles(() => ({
|
|
|
37
37
|
incorrect: correctStyle(color.incorrect()),
|
|
38
38
|
box: {
|
|
39
39
|
fontSize: 'inherit',
|
|
40
|
+
display: 'inline-block',
|
|
41
|
+
verticalAlign: 'middle',
|
|
40
42
|
},
|
|
41
43
|
outlinedInput: {
|
|
42
44
|
padding: '2px',
|
|
@@ -60,8 +62,11 @@ export default withStyles(() => ({
|
|
|
60
62
|
spellCheck,
|
|
61
63
|
...rest
|
|
62
64
|
} = props;
|
|
65
|
+
|
|
63
66
|
const label = typeof correct === 'boolean' ? (correct ? 'correct' : 'incorrect') : undefined;
|
|
64
|
-
const inputProps = charactersLimit
|
|
67
|
+
const inputProps = charactersLimit
|
|
68
|
+
? { maxLength: charactersLimit, 'aria-label': 'Enter answer' }
|
|
69
|
+
: { 'aria-label': 'Enter answer' };
|
|
65
70
|
|
|
66
71
|
if (width) {
|
|
67
72
|
inputProps.style = {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import
|
|
3
|
+
import Button from '@material-ui/core/Button';
|
|
4
|
+
import InputLabel from '@material-ui/core/InputLabel';
|
|
5
|
+
import Menu from '@material-ui/core/Menu';
|
|
4
6
|
import MenuItem from '@material-ui/core/MenuItem';
|
|
5
|
-
import
|
|
7
|
+
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
|
|
8
|
+
import Close from '@material-ui/icons/Close';
|
|
9
|
+
import Check from '@material-ui/icons/Check';
|
|
6
10
|
import { withStyles } from '@material-ui/core/styles';
|
|
11
|
+
import classNames from 'classnames';
|
|
12
|
+
|
|
7
13
|
import { color } from '@pie-lib/render-ui';
|
|
14
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
8
15
|
|
|
9
16
|
class Dropdown extends React.Component {
|
|
10
17
|
static propTypes = {
|
|
@@ -22,73 +29,148 @@ class Dropdown extends React.Component {
|
|
|
22
29
|
super(props);
|
|
23
30
|
|
|
24
31
|
this.state = {
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
anchorEl: null,
|
|
33
|
+
highlightedOptionId: null,
|
|
27
34
|
};
|
|
35
|
+
|
|
36
|
+
this.elementRefs = [];
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
componentDidUpdate() {
|
|
40
|
+
this.elementRefs.forEach((ref) => {
|
|
41
|
+
if (ref) {
|
|
42
|
+
renderMath(ref);
|
|
43
|
+
}
|
|
34
44
|
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleClick = (event) => this.setState({ anchorEl: event.currentTarget });
|
|
48
|
+
|
|
49
|
+
handleClose = () => this.setState({ anchorEl: null });
|
|
50
|
+
|
|
51
|
+
handleHighlight = (index) => {
|
|
52
|
+
const highlightedOptionId = `dropdown-option-${this.props.id}-${index}`;
|
|
53
|
+
|
|
54
|
+
this.setState({ highlightedOptionId });
|
|
35
55
|
};
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
});
|
|
57
|
+
handleSelect = (value, index) => {
|
|
58
|
+
this.props.onChange(this.props.id, value);
|
|
59
|
+
this.handleHighlight(index);
|
|
60
|
+
this.handleClose();
|
|
42
61
|
};
|
|
43
62
|
|
|
63
|
+
getLabel(choices, value) {
|
|
64
|
+
const found = (choices || []).find((choice) => choice.value === value);
|
|
65
|
+
|
|
66
|
+
return found ? found.label.trim() : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
44
69
|
render() {
|
|
45
|
-
const { classes, id, correct, disabled, value,
|
|
70
|
+
const { classes, id, correct, disabled, value, choices, showCorrectAnswer, singleQuery, correctValue } = this.props;
|
|
71
|
+
|
|
72
|
+
const { anchorEl } = this.state;
|
|
73
|
+
const open = Boolean(anchorEl);
|
|
74
|
+
const buttonId = `dropdown-button-${id}`;
|
|
75
|
+
const menuId = `dropdown-menu-${id}`;
|
|
76
|
+
const valueDisplayId = `dropdown-value-${id}`;
|
|
77
|
+
|
|
78
|
+
// Determine the class for disabled state, view mode and evaluate mode
|
|
79
|
+
let disabledClass;
|
|
80
|
+
// Reset elementRefs before each render to avoid stale references
|
|
81
|
+
this.elementRefs = [];
|
|
82
|
+
|
|
83
|
+
if (disabled && correct !== undefined) {
|
|
84
|
+
disabledClass = correct || showCorrectAnswer ? classes.disabledCorrect : classes.disabledIncorrect;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create distinct, visually hidden labels for each dropdown
|
|
88
|
+
const incrementedId = parseInt(id, 10) + 1;
|
|
89
|
+
const labelId = singleQuery ? 'Query-label' : `Query-label-${incrementedId}`;
|
|
90
|
+
const labelText = singleQuery ? 'Query' : `Query ${incrementedId}`;
|
|
91
|
+
|
|
92
|
+
// Changed from Select to Button for dropdown to enhance accessibility. This modification offers explicit control over aria attributes and focuses management, ensuring the dropdown is compliant with accessibility standards. The use of Button and Menu components allows for better handling of keyboard interactions and provides accessible labels and menus, aligning with WCAG guidelines and improving usability for assistive technology users.
|
|
46
93
|
|
|
47
|
-
|
|
94
|
+
let correctnessIcon = null;
|
|
95
|
+
if (disabled && correct !== undefined) {
|
|
96
|
+
correctnessIcon =
|
|
97
|
+
correct || showCorrectAnswer ? (
|
|
98
|
+
<Check className={classNames(classes.correctnessIndicatorIcon, classes.correctIcon)} />
|
|
99
|
+
) : (
|
|
100
|
+
<Close className={classNames(classes.correctnessIndicatorIcon, classes.incorrectIcon)} />
|
|
101
|
+
);
|
|
102
|
+
}
|
|
48
103
|
|
|
49
104
|
return (
|
|
50
|
-
|
|
51
|
-
classes={
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
<>
|
|
106
|
+
<InputLabel className={classes.srOnly} id={labelId}>
|
|
107
|
+
{labelText}
|
|
108
|
+
</InputLabel>
|
|
109
|
+
<Button
|
|
110
|
+
aria-controls={open ? menuId : undefined}
|
|
111
|
+
aria-haspopup="listbox"
|
|
112
|
+
aria-expanded={open ? 'true' : undefined}
|
|
113
|
+
aria-activedescendant={this.state.highlightedOptionId}
|
|
114
|
+
onClick={this.handleClick}
|
|
115
|
+
classes={{
|
|
116
|
+
root: classes.root,
|
|
117
|
+
disabled: disabledClass,
|
|
118
|
+
}}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
id={buttonId}
|
|
121
|
+
role="combobox"
|
|
122
|
+
aria-label="Select answer"
|
|
123
|
+
aria-labelledby={valueDisplayId}
|
|
124
|
+
>
|
|
125
|
+
{correctnessIcon}
|
|
126
|
+
<span
|
|
127
|
+
id={valueDisplayId}
|
|
128
|
+
className={classes.label}
|
|
129
|
+
dangerouslySetInnerHTML={{
|
|
130
|
+
__html: correctValue ? correctValue : this.getLabel(choices, value) ? this.getLabel(choices, value) : '',
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
<ArrowDropDownIcon />
|
|
134
|
+
</Button>
|
|
135
|
+
<Menu
|
|
136
|
+
id={menuId}
|
|
137
|
+
anchorEl={anchorEl}
|
|
138
|
+
className={classes.selectMenu}
|
|
139
|
+
keepMounted
|
|
140
|
+
open={open}
|
|
141
|
+
onClose={this.handleClose}
|
|
142
|
+
MenuListProps={{
|
|
143
|
+
'aria-labelledby': buttonId,
|
|
144
|
+
role: 'listbox',
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
{(choices || []).map((c, index) => {
|
|
148
|
+
const optionId = `dropdown-option-${id}-${index}`;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<MenuItem
|
|
152
|
+
id={optionId}
|
|
153
|
+
classes={{ root: classes.menuRoot, selected: classes.selected }}
|
|
154
|
+
key={`${c.label}-${index}`}
|
|
155
|
+
value={c.value}
|
|
156
|
+
onClick={() => this.handleSelect(c.value, index)}
|
|
157
|
+
role="option"
|
|
158
|
+
aria-selected={this.state.highlightedOptionId === optionId ? 'true' : undefined}
|
|
159
|
+
>
|
|
160
|
+
<span
|
|
161
|
+
ref={(ref) => (this.elementRefs[index] = ref)}
|
|
162
|
+
className={classes.label}
|
|
163
|
+
dangerouslySetInnerHTML={{ __html: c.label }}
|
|
164
|
+
/>
|
|
165
|
+
<span
|
|
166
|
+
className={classes.label}
|
|
167
|
+
dangerouslySetInnerHTML={{ __html: c.value === value ? ' ✓' : '' }}
|
|
168
|
+
/>
|
|
169
|
+
</MenuItem>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</Menu>
|
|
173
|
+
</>
|
|
92
174
|
);
|
|
93
175
|
}
|
|
94
176
|
}
|
|
@@ -96,34 +178,49 @@ class Dropdown extends React.Component {
|
|
|
96
178
|
const styles = () => ({
|
|
97
179
|
root: {
|
|
98
180
|
color: color.text(),
|
|
181
|
+
border: `1px solid ${color.text()}`,
|
|
182
|
+
borderRadius: '4px',
|
|
183
|
+
justifyContent: 'space-between',
|
|
99
184
|
backgroundColor: color.background(),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
185
|
+
position: 'relative',
|
|
186
|
+
height: '45px',
|
|
187
|
+
width: 'fit-content',
|
|
188
|
+
margin: '2px',
|
|
189
|
+
textTransform: 'none',
|
|
190
|
+
'& span': {
|
|
191
|
+
paddingRight: '5px',
|
|
192
|
+
},
|
|
193
|
+
'& svg': {
|
|
194
|
+
position: 'absolute',
|
|
195
|
+
right: 0,
|
|
196
|
+
top: 'calc(50% - 12px)',
|
|
197
|
+
pointerEvents: 'none',
|
|
106
198
|
color: color.text(),
|
|
107
|
-
|
|
199
|
+
marginLeft: '5px',
|
|
108
200
|
},
|
|
109
201
|
},
|
|
110
|
-
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
}
|
|
202
|
+
disabledCorrect: {
|
|
203
|
+
borderWidth: '2px',
|
|
204
|
+
borderColor: color.correct(),
|
|
205
|
+
color: `${color.text()} !important`,
|
|
206
|
+
},
|
|
207
|
+
disabledIncorrect: {
|
|
208
|
+
borderWidth: '2px',
|
|
209
|
+
borderColor: color.incorrectWithIcon(),
|
|
210
|
+
color: `${color.text()} !important`,
|
|
114
211
|
},
|
|
115
212
|
selectMenu: {
|
|
116
213
|
backgroundColor: color.background(),
|
|
214
|
+
border: `1px solid ${color.correct()} !important`,
|
|
117
215
|
'&:hover': {
|
|
216
|
+
border: `1px solid ${color.text()} `,
|
|
118
217
|
borderColor: 'initial',
|
|
119
218
|
},
|
|
120
219
|
'&:focus': {
|
|
220
|
+
border: `1px solid ${color.text()}`,
|
|
121
221
|
borderColor: 'initial',
|
|
122
222
|
},
|
|
123
223
|
},
|
|
124
|
-
icon: {
|
|
125
|
-
color: color.text(),
|
|
126
|
-
},
|
|
127
224
|
selected: {
|
|
128
225
|
color: `${color.text()} !important`,
|
|
129
226
|
backgroundColor: `${color.background()} !important`,
|
|
@@ -155,6 +252,30 @@ const styles = () => ({
|
|
|
155
252
|
label: {
|
|
156
253
|
fontSize: 'max(1rem, 14px)',
|
|
157
254
|
},
|
|
255
|
+
srOnly: {
|
|
256
|
+
position: 'absolute',
|
|
257
|
+
left: '-10000px',
|
|
258
|
+
top: 'auto',
|
|
259
|
+
width: '1px',
|
|
260
|
+
height: '1px',
|
|
261
|
+
overflow: 'hidden',
|
|
262
|
+
},
|
|
263
|
+
correctnessIndicatorIcon: {
|
|
264
|
+
color: `${color.white()} !important`,
|
|
265
|
+
position: 'absolute',
|
|
266
|
+
top: '-8px !important',
|
|
267
|
+
left: '-8px',
|
|
268
|
+
marginLeft: '0 !important',
|
|
269
|
+
borderRadius: '50%',
|
|
270
|
+
fontSize: '16px',
|
|
271
|
+
padding: '2px',
|
|
272
|
+
},
|
|
273
|
+
correctIcon: {
|
|
274
|
+
backgroundColor: color.correct(),
|
|
275
|
+
},
|
|
276
|
+
incorrectIcon: {
|
|
277
|
+
backgroundColor: color.incorrectWithIcon(),
|
|
278
|
+
},
|
|
158
279
|
});
|
|
159
280
|
|
|
160
281
|
export default withStyles(styles)(Dropdown);
|