@pie-lib/config-ui 12.0.0-beta.4 → 12.0.0-next.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 (120) hide show
  1. package/CHANGELOG.json +8 -1653
  2. package/CHANGELOG.md +540 -58
  3. package/LICENSE.md +5 -0
  4. package/NEXT.CHANGELOG.json +1 -0
  5. package/lib/alert-dialog.js +44 -20
  6. package/lib/alert-dialog.js.map +1 -1
  7. package/lib/checkbox.js +59 -61
  8. package/lib/checkbox.js.map +1 -1
  9. package/lib/choice-configuration/feedback-menu.js +30 -65
  10. package/lib/choice-configuration/feedback-menu.js.map +1 -1
  11. package/lib/choice-configuration/index.js +231 -244
  12. package/lib/choice-configuration/index.js.map +1 -1
  13. package/lib/choice-utils.js +7 -19
  14. package/lib/choice-utils.js.map +1 -1
  15. package/lib/feedback-config/feedback-selector.js +89 -115
  16. package/lib/feedback-config/feedback-selector.js.map +1 -1
  17. package/lib/feedback-config/group.js +28 -42
  18. package/lib/feedback-config/group.js.map +1 -1
  19. package/lib/feedback-config/index.js +55 -87
  20. package/lib/feedback-config/index.js.map +1 -1
  21. package/lib/form-section.js +32 -34
  22. package/lib/form-section.js.map +1 -1
  23. package/lib/help.js +41 -80
  24. package/lib/help.js.map +1 -1
  25. package/lib/index.js +2 -32
  26. package/lib/index.js.map +1 -1
  27. package/lib/input.js +24 -57
  28. package/lib/input.js.map +1 -1
  29. package/lib/inputs.js +62 -88
  30. package/lib/inputs.js.map +1 -1
  31. package/lib/langs.js +59 -102
  32. package/lib/langs.js.map +1 -1
  33. package/lib/layout/config-layout.js +95 -67
  34. package/lib/layout/config-layout.js.map +1 -1
  35. package/lib/layout/index.js +1 -4
  36. package/lib/layout/index.js.map +1 -1
  37. package/lib/layout/layout-contents.js +130 -75
  38. package/lib/layout/layout-contents.js.map +1 -1
  39. package/lib/layout/settings-box.js +28 -58
  40. package/lib/layout/settings-box.js.map +1 -1
  41. package/lib/mui-box/index.js +42 -58
  42. package/lib/mui-box/index.js.map +1 -1
  43. package/lib/number-text-field-custom.js +164 -152
  44. package/lib/number-text-field-custom.js.map +1 -1
  45. package/lib/number-text-field.js +87 -119
  46. package/lib/number-text-field.js.map +1 -1
  47. package/lib/radio-with-label.js +33 -26
  48. package/lib/radio-with-label.js.map +1 -1
  49. package/lib/settings/display-size.js +17 -33
  50. package/lib/settings/display-size.js.map +1 -1
  51. package/lib/settings/index.js +26 -46
  52. package/lib/settings/index.js.map +1 -1
  53. package/lib/settings/panel.js +202 -221
  54. package/lib/settings/panel.js.map +1 -1
  55. package/lib/settings/settings-radio-label.js +37 -29
  56. package/lib/settings/settings-radio-label.js.map +1 -1
  57. package/lib/settings/toggle.js +40 -33
  58. package/lib/settings/toggle.js.map +1 -1
  59. package/lib/tabs/index.js +26 -57
  60. package/lib/tabs/index.js.map +1 -1
  61. package/lib/tags-input/index.js +51 -100
  62. package/lib/tags-input/index.js.map +1 -1
  63. package/lib/two-choice.js +47 -91
  64. package/lib/two-choice.js.map +1 -1
  65. package/lib/with-stateful-model.js +11 -34
  66. package/lib/with-stateful-model.js.map +1 -1
  67. package/package.json +22 -11
  68. package/src/__tests__/alert-dialog.test.jsx +283 -0
  69. package/src/__tests__/checkbox.test.jsx +249 -0
  70. package/src/__tests__/choice-utils.test.js +12 -0
  71. package/src/__tests__/form-section.test.jsx +334 -0
  72. package/src/__tests__/help.test.jsx +184 -0
  73. package/src/__tests__/input.test.jsx +192 -0
  74. package/src/__tests__/langs.test.jsx +457 -0
  75. package/src/__tests__/number-text-field-custom.test.jsx +438 -0
  76. package/src/__tests__/number-text-field.test.jsx +341 -0
  77. package/src/__tests__/radio-with-label.test.jsx +259 -0
  78. package/src/__tests__/settings-panel.test.js +187 -0
  79. package/src/__tests__/settings.test.jsx +515 -0
  80. package/src/__tests__/tabs.test.jsx +193 -0
  81. package/src/__tests__/two-choice.test.js +110 -0
  82. package/src/__tests__/with-stateful-model.test.jsx +145 -0
  83. package/src/alert-dialog.jsx +31 -16
  84. package/src/checkbox.jsx +45 -39
  85. package/src/choice-configuration/__tests__/feedback-menu.test.jsx +163 -0
  86. package/src/choice-configuration/__tests__/index.test.jsx +234 -0
  87. package/src/choice-configuration/feedback-menu.jsx +15 -28
  88. package/src/choice-configuration/index.jsx +233 -182
  89. package/src/choice-utils.js +1 -1
  90. package/src/feedback-config/__tests__/feedback-config.test.jsx +141 -0
  91. package/src/feedback-config/__tests__/feedback-selector.test.jsx +107 -0
  92. package/src/feedback-config/feedback-selector.jsx +65 -60
  93. package/src/feedback-config/group.jsx +26 -29
  94. package/src/feedback-config/index.jsx +59 -47
  95. package/src/form-section.jsx +26 -18
  96. package/src/help.jsx +27 -36
  97. package/src/index.js +2 -5
  98. package/src/input.jsx +9 -9
  99. package/src/inputs.jsx +36 -50
  100. package/src/langs.jsx +57 -73
  101. package/src/layout/__tests__/config.layout.test.jsx +59 -0
  102. package/src/layout/__tests__/layout-content.test.jsx +3 -0
  103. package/src/layout/config-layout.jsx +70 -37
  104. package/src/layout/layout-contents.jsx +96 -39
  105. package/src/layout/settings-box.jsx +22 -21
  106. package/src/mui-box/index.jsx +37 -45
  107. package/src/number-text-field-custom.jsx +136 -81
  108. package/src/number-text-field.jsx +59 -37
  109. package/src/radio-with-label.jsx +28 -12
  110. package/src/settings/display-size.jsx +14 -13
  111. package/src/settings/index.js +20 -12
  112. package/src/settings/panel.jsx +147 -110
  113. package/src/settings/settings-radio-label.jsx +29 -13
  114. package/src/settings/toggle.jsx +39 -20
  115. package/src/tabs/index.jsx +15 -19
  116. package/src/tags-input/__tests__/index.test.jsx +113 -0
  117. package/src/tags-input/index.jsx +42 -47
  118. package/src/two-choice.jsx +19 -23
  119. package/src/with-stateful-model.jsx +5 -5
  120. package/README.md +0 -12
@@ -1,110 +1,181 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { withStyles } from '@material-ui/core/styles';
4
- import TextField from '@material-ui/core/TextField';
5
- import classNames from 'classnames';
3
+ import { styled } from '@mui/material/styles';
4
+ import TextField from '@mui/material/TextField';
5
+ import ActionDelete from '@mui/icons-material/Delete';
6
+ import ArrowRight from '@mui/icons-material/SubdirectoryArrowRight';
7
+ import IconButton from '@mui/material/IconButton';
6
8
  import { InputContainer } from '@pie-lib/render-ui';
7
- import EditableHtml from '@pie-lib/editable-html';
9
+ // import EditableHtml from '@pie-lib/editable-html';
8
10
  import { InputCheckbox, InputRadio } from '../inputs';
9
11
  import FeedbackMenu from './feedback-menu';
10
- import ActionDelete from '@material-ui/icons/Delete';
11
- import ArrowRight from '@material-ui/icons/SubdirectoryArrowRight';
12
- import IconButton from '@material-ui/core/IconButton';
13
-
14
- const EditableHtmlContainer = withStyles(theme => ({
15
- labelContainer: {},
16
- editorHolder: {
17
- marginTop: theme.spacing.unit * 2
18
- }
19
- }))(
20
- ({
21
- label,
22
- classes,
23
- onChange,
24
- value,
25
- className,
26
- imageSupport,
27
- disabled,
28
- spellCheck,
29
- nonEmpty,
30
- toolbarOpts,
31
- error,
32
- maxImageWidth,
33
- maxImageHeight
34
- }) => {
35
- const names = classNames(classes.labelContainer, className);
36
12
 
37
- return (
38
- <InputContainer label={label} className={names}>
39
- <div className={classes.editorHolder}>
40
- <EditableHtml
41
- markup={value || ''}
42
- disabled={disabled}
43
- spellCheck={spellCheck}
44
- nonEmpty={nonEmpty}
45
- onChange={onChange}
46
- imageSupport={imageSupport}
47
- className={classes.editor}
48
- toolbarOpts={toolbarOpts}
49
- error={error}
50
- maxImageWidth={maxImageWidth}
51
- maxImageHeight={maxImageHeight}
52
- />
53
- </div>
54
- </InputContainer>
55
- );
56
- }
57
- );
58
-
59
- const Feedback = withStyles(() => ({
60
- text: {
61
- width: '100%'
62
- },
63
- feedbackContainer: {
64
- position: 'relative'
65
- },
66
- arrowIcon: {
67
- fill: '#ccc',
68
- left: -56,
69
- position: 'absolute',
70
- top: 20
71
- }
72
- }))(({ value, onChange, type, correct, classes, defaults, toolbarOpts }) => {
13
+ // - mathquill error window not defined
14
+ let EditableHtml;
15
+ if (typeof window !== 'undefined') {
16
+ EditableHtml = require('@pie-lib/editable-html')['default'];
17
+ }
18
+
19
+ const StyledEditorHolder = styled('div')(({ theme }) => ({
20
+ marginTop: theme.spacing(2),
21
+ }));
22
+
23
+ const EditableHtmlContainer = ({
24
+ label,
25
+ onChange,
26
+ value,
27
+ className,
28
+ imageSupport,
29
+ disableImageAlignmentButtons,
30
+ disabled,
31
+ spellCheck,
32
+ nonEmpty,
33
+ pluginOpts,
34
+ toolbarOpts,
35
+ error,
36
+ maxImageWidth,
37
+ maxImageHeight,
38
+ uploadSoundSupport,
39
+ mathMlOptions = {},
40
+ }) => {
41
+
42
+ return (
43
+ <InputContainer label={label} className={className}>
44
+ <StyledEditorHolder>
45
+ <EditableHtml
46
+ markup={value || ''}
47
+ disabled={disabled}
48
+ spellCheck={spellCheck}
49
+ nonEmpty={nonEmpty}
50
+ onChange={onChange}
51
+ imageSupport={imageSupport}
52
+ disableImageAlignmentButtons={disableImageAlignmentButtons}
53
+ pluginProps={pluginOpts || {}}
54
+ toolbarOpts={toolbarOpts}
55
+ error={error}
56
+ maxImageWidth={maxImageWidth}
57
+ maxImageHeight={maxImageHeight}
58
+ uploadSoundSupport={uploadSoundSupport}
59
+ languageCharactersProps={[{ language: 'spanish' }, { language: 'special' }]}
60
+ mathMlOptions={mathMlOptions}
61
+ />
62
+ </StyledEditorHolder>
63
+ </InputContainer>
64
+ );
65
+ };
66
+
67
+ const StyledFeedbackContainer = styled('div')(() => ({
68
+ position: 'relative',
69
+ }));
70
+
71
+ const StyledArrowIcon = styled(ArrowRight)(({ theme }) => ({
72
+ fill: theme.palette.grey[400],
73
+ left: -56,
74
+ position: 'absolute',
75
+ top: 40,
76
+ }));
77
+
78
+ const StyledTextField = styled(TextField)(({ theme }) => ({
79
+ width: '100%',
80
+ marginTop: theme.spacing(2),
81
+ }));
82
+
83
+ const StyledEditableHtmlContainer = styled(EditableHtmlContainer)(({ theme }) => ({
84
+ width: '100%',
85
+ marginTop: theme.spacing(2),
86
+ }));
87
+
88
+ const Feedback = ({ value, onChange, type, correct, defaults, toolbarOpts, mathMlOptions = {} }) => {
73
89
  if (!type || type === 'none') {
74
90
  return null;
75
91
  } else if (type === 'default') {
76
92
  return (
77
- <div className={classes.feedbackContainer}>
78
- <ArrowRight className={classes.arrowIcon} />
79
- <TextField
80
- className={classes.text}
93
+ <StyledFeedbackContainer>
94
+ <StyledArrowIcon />
95
+ <StyledTextField
81
96
  label="Feedback Text"
82
97
  value={correct ? defaults.correct : defaults.incorrect}
98
+ variant="standard"
83
99
  />
84
- </div>
100
+ </StyledFeedbackContainer>
85
101
  );
86
102
  } else {
87
103
  return (
88
- <div className={classes.feedbackContainer}>
89
- <ArrowRight className={classes.arrowIcon} />
90
- <EditableHtmlContainer
91
- className={classes.text}
104
+ <StyledFeedbackContainer>
105
+ <StyledArrowIcon />
106
+ <StyledEditableHtmlContainer
92
107
  label="Feedback Text"
93
108
  value={value}
94
109
  onChange={onChange}
95
110
  toolbarOpts={toolbarOpts}
111
+ mathMlOptions={mathMlOptions}
96
112
  />
97
- </div>
113
+ </StyledFeedbackContainer>
98
114
  );
99
115
  }
100
- });
116
+ };
117
+
118
+ const StyledIndex = styled('span')(({ theme }) => ({
119
+ paddingRight: theme.spacing(1),
120
+ paddingTop: theme.spacing(3),
121
+ }));
122
+
123
+ const StyledTopRow = styled('div')(() => ({
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ }));
127
+
128
+ const StyledToggle = styled('div')(({ theme }) => ({
129
+ flex: '0 1 auto',
130
+ paddingTop: theme.spacing(0.5),
131
+ paddingBottom: 0,
132
+ marginRight: 0,
133
+ marginLeft: theme.spacing(1),
134
+ }));
135
+
136
+ const StyledFeedback = styled('div')(({ theme }) => ({
137
+ flex: '0 1 auto',
138
+ paddingTop: theme.spacing(2),
139
+ paddingLeft: 0,
140
+ marginLeft: 0,
141
+ marginRight: theme.spacing(1),
142
+ }));
143
+
144
+ const StyledFeedbackIcon = styled('div')(() => ({
145
+ margin: 0,
146
+ width: 'inherit',
147
+ }));
148
+
149
+ const StyledDeleteIcon = styled('div')(() => ({
150
+ margin: 0,
151
+ width: 'inherit',
152
+ }));
153
+
154
+ const StyledDelete = styled('div')(({ theme }) => ({
155
+ flex: '0 1 auto',
156
+ paddingTop: theme.spacing(2),
157
+ paddingLeft: 0,
158
+ marginLeft: 0,
159
+ }));
160
+
161
+ const StyledMiddleColumn = styled('div')(({ theme }) => ({
162
+ display: 'flex',
163
+ flex: 1,
164
+ flexDirection: 'column',
165
+ marginRight: theme.spacing(1),
166
+ }));
167
+
168
+ const StyledErrorText = styled('div')(({ theme }) => ({
169
+ fontSize: theme.typography.fontSize - 2,
170
+ color: theme.palette.error.main,
171
+ }));
101
172
 
102
173
  export class ChoiceConfiguration extends React.Component {
103
174
  static propTypes = {
104
- classes: PropTypes.object.isRequired,
105
175
  noLabels: PropTypes.bool,
106
176
  useLetterOrdering: PropTypes.bool,
107
177
  className: PropTypes.string,
178
+ error: PropTypes.string,
108
179
  mode: PropTypes.oneOf(['checkbox', 'radio']),
109
180
  defaultFeedback: PropTypes.object.isRequired,
110
181
  disabled: PropTypes.bool,
@@ -115,19 +186,26 @@ export class ChoiceConfiguration extends React.Component {
115
186
  correct: PropTypes.bool,
116
187
  feedback: PropTypes.shape({
117
188
  type: PropTypes.string,
118
- value: PropTypes.string
119
- })
189
+ value: PropTypes.string,
190
+ }),
120
191
  }),
121
192
  onDelete: PropTypes.func,
122
193
  onChange: PropTypes.func,
123
194
  index: PropTypes.number,
124
195
  imageSupport: PropTypes.shape({
125
196
  add: PropTypes.func.isRequired,
126
- delete: PropTypes.func.isRequired
197
+ delete: PropTypes.func.isRequired,
127
198
  }),
199
+ disableImageAlignmentButtons: PropTypes.bool,
128
200
  allowFeedBack: PropTypes.bool,
129
201
  allowDelete: PropTypes.bool,
130
- toolbarOpts: PropTypes.object
202
+ noCorrectAnswerError: PropTypes.string,
203
+ spellCheck: PropTypes.bool,
204
+ pluginOpts: PropTypes.object,
205
+ toolbarOpts: PropTypes.object,
206
+ uploadSoundSupport: PropTypes.object,
207
+ maxImageWidth: PropTypes.number,
208
+ maxImageHeight: PropTypes.number,
131
209
  };
132
210
 
133
211
  static defaultProps = {
@@ -135,11 +213,12 @@ export class ChoiceConfiguration extends React.Component {
135
213
  noLabels: false,
136
214
  useLetterOrdering: false,
137
215
  allowFeedBack: true,
138
- allowDelete: true
216
+ allowDelete: true,
139
217
  };
140
218
 
141
- _changeFn = key => update => {
219
+ _changeFn = (key) => (update) => {
142
220
  const { data, onChange } = this.props;
221
+
143
222
  if (onChange) {
144
223
  onChange({ ...data, [key]: update });
145
224
  }
@@ -147,7 +226,7 @@ export class ChoiceConfiguration extends React.Component {
147
226
 
148
227
  onLabelChange = this._changeFn('label');
149
228
 
150
- onCheckedChange = event => {
229
+ onCheckedChange = (event) => {
151
230
  const correct = event.target.checked;
152
231
  const { data, onChange } = this.props;
153
232
 
@@ -156,7 +235,7 @@ export class ChoiceConfiguration extends React.Component {
156
235
  }
157
236
  };
158
237
 
159
- onFeedbackValueChange = v => {
238
+ onFeedbackValueChange = (v) => {
160
239
  const { data, onChange } = this.props;
161
240
 
162
241
  if (data.feedback.type !== 'custom') {
@@ -168,9 +247,10 @@ export class ChoiceConfiguration extends React.Component {
168
247
  if (onChange) onChange({ ...data, feedback: fb });
169
248
  };
170
249
 
171
- onFeedbackTypeChange = t => {
250
+ onFeedbackTypeChange = (t) => {
172
251
  const { data, onChange } = this.props;
173
252
  const fb = { ...data.feedback, type: t };
253
+
174
254
  if (fb.type !== 'custom') {
175
255
  fb.value = undefined;
176
256
  }
@@ -181,7 +261,6 @@ export class ChoiceConfiguration extends React.Component {
181
261
  render() {
182
262
  const {
183
263
  data,
184
- classes,
185
264
  mode,
186
265
  onDelete,
187
266
  defaultFeedback,
@@ -190,46 +269,60 @@ export class ChoiceConfiguration extends React.Component {
190
269
  noLabels,
191
270
  useLetterOrdering,
192
271
  imageSupport,
272
+ disableImageAlignmentButtons,
193
273
  disabled,
194
274
  spellCheck,
195
275
  nonEmpty,
196
276
  allowFeedBack,
197
277
  allowDelete,
278
+ pluginOpts,
198
279
  toolbarOpts,
199
280
  error,
200
- noCorrectAnswerError
281
+ noCorrectAnswerError,
282
+ uploadSoundSupport,
283
+ maxImageWidth,
284
+ maxImageHeight,
285
+ mathMlOptions = {},
201
286
  } = this.props;
202
287
 
203
288
  const InputToggle = mode === 'checkbox' ? InputCheckbox : InputRadio;
204
- const names = classNames(classes.choiceConfiguration, className);
289
+
205
290
  return (
206
- <div className={names}>
207
- <div className={classes.topRow}>
291
+ <StyledTopRow>
208
292
  {index > 0 && (
209
- <span className={classes.index} type="title">
293
+ <StyledIndex type="title">
210
294
  {useLetterOrdering ? String.fromCharCode(96 + index).toUpperCase() : index}
211
- </span>
295
+ </StyledIndex>
212
296
  )}
213
- <InputToggle
214
- className={classes.toggle}
215
- onChange={this.onCheckedChange}
216
- label={!noLabels ? 'Correct' : ''}
217
- checked={!!data.correct}
218
- error={noCorrectAnswerError}
219
- />
220
- <div className={classes.middleColumn}>
221
- <EditableHtmlContainer
222
- label={!noLabels ? 'Label' : ''}
223
- value={data.label}
224
- onChange={this.onLabelChange}
225
- imageSupport={imageSupport}
226
- disabled={disabled}
227
- spellCheck={spellCheck}
228
- nonEmpty={nonEmpty}
229
- toolbarOpts={toolbarOpts}
230
- error={error}
297
+
298
+ <StyledToggle>
299
+ <InputToggle
300
+ onChange={this.onCheckedChange}
301
+ label={!noLabels ? 'Correct' : ''}
302
+ checked={!!data.correct}
303
+ error={noCorrectAnswerError}
231
304
  />
232
- {error && <div className={classes.errorText}>{error}</div>}
305
+ </StyledToggle>
306
+
307
+ <StyledMiddleColumn>
308
+ <EditableHtmlContainer
309
+ label={!noLabels ? 'Label' : ''}
310
+ value={data.label}
311
+ onChange={this.onLabelChange}
312
+ imageSupport={imageSupport}
313
+ disableImageAlignmentButtons={disableImageAlignmentButtons}
314
+ disabled={disabled}
315
+ spellCheck={spellCheck}
316
+ nonEmpty={nonEmpty}
317
+ pluginOpts={pluginOpts}
318
+ toolbarOpts={toolbarOpts}
319
+ error={error}
320
+ uploadSoundSupport={uploadSoundSupport}
321
+ mathMlOptions={mathMlOptions}
322
+ maxImageWidth={maxImageWidth}
323
+ maxImageHeight={maxImageHeight}
324
+ />
325
+ {error && <StyledErrorText>{error}</StyledErrorText>}
233
326
 
234
327
  {allowFeedBack && (
235
328
  <Feedback
@@ -240,80 +333,38 @@ export class ChoiceConfiguration extends React.Component {
240
333
  toolbarOpts={toolbarOpts}
241
334
  />
242
335
  )}
243
- </div>
336
+ </StyledMiddleColumn>
337
+
244
338
  {allowFeedBack && (
245
- <InputContainer className={classes.feedback} label={!noLabels ? 'Feedback' : ''}>
246
- <FeedbackMenu
247
- onChange={this.onFeedbackTypeChange}
248
- value={data.feedback}
249
- classes={{
250
- icon: classes.feedbackIcon
251
- }}
252
- />
253
- </InputContainer>
339
+ <StyledFeedback>
340
+ <InputContainer label={!noLabels ? 'Feedback' : ''}>
341
+ <StyledFeedbackIcon>
342
+ <FeedbackMenu
343
+ onChange={this.onFeedbackTypeChange}
344
+ value={data.feedback}
345
+ />
346
+ </StyledFeedbackIcon>
347
+ </InputContainer>
348
+ </StyledFeedback>
254
349
  )}
350
+
255
351
  {allowDelete && (
256
- <InputContainer className={classes.delete} label={!noLabels ? 'Delete' : ''}>
257
- <IconButton aria-label="delete" className={classes.deleteIcon} onClick={onDelete}>
258
- <ActionDelete />
259
- </IconButton>
260
- </InputContainer>
352
+ <StyledDelete>
353
+ <InputContainer label={!noLabels ? 'Delete' : ''}>
354
+ <StyledDeleteIcon>
355
+ <IconButton
356
+ aria-label="delete"
357
+ onClick={onDelete}
358
+ size="large">
359
+ <ActionDelete />
360
+ </IconButton>
361
+ </StyledDeleteIcon>
362
+ </InputContainer>
363
+ </StyledDelete>
261
364
  )}
262
- </div>
263
- </div>
365
+ </StyledTopRow>
264
366
  );
265
367
  }
266
368
  }
267
369
 
268
- const styles = theme => ({
269
- index: {
270
- padding: '24px 10px 0 0'
271
- },
272
- choiceConfiguration: {},
273
- topRow: {
274
- display: 'flex'
275
- },
276
- value: {
277
- flex: '0.5',
278
- paddingRight: theme.spacing.unit
279
- },
280
- editorHolder: {
281
- marginTop: theme.spacing.unit * 2
282
- },
283
- toggle: {
284
- flex: '0 1 auto'
285
- },
286
- feedback: {
287
- flex: '0 1 auto',
288
- paddingTop: theme.spacing.unit,
289
- paddingLeft: 0,
290
- marginLeft: 0,
291
- paddingRight: theme.spacing.unit * 3
292
- },
293
- feedbackIcon: {
294
- margin: 0,
295
- paddingLeft: 0,
296
- width: 'inherit'
297
- },
298
- deleteIcon: {
299
- margin: 0,
300
- width: 'inherit'
301
- },
302
- delete: {
303
- flex: '0 1 auto',
304
- paddingTop: theme.spacing.unit,
305
- paddingLeft: 0,
306
- marginLeft: 0
307
- },
308
- middleColumn: {
309
- display: 'flex',
310
- flex: 1,
311
- flexDirection: 'column'
312
- },
313
- errorText: {
314
- fontSize: '12px',
315
- color: 'red'
316
- }
317
- });
318
-
319
- export default withStyles(styles)(ChoiceConfiguration);
370
+ export default ChoiceConfiguration;
@@ -5,7 +5,7 @@ import includes from 'lodash/includes';
5
5
  * @param {Object} model the model to normalize
6
6
  * @return {Object} the updated model
7
7
  */
8
- export const normalizeChoices = model => {
8
+ export const normalizeChoices = (model) => {
9
9
  const choices = model.choices.map((c, index) => {
10
10
  if (!c.value) {
11
11
  c.value = `${index}`;
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { FeedbackConfig, buildDefaults } from '../index';
5
+
6
+ // Mock FeedbackSelector to simplify testing
7
+ jest.mock('../feedback-selector', () => {
8
+ return function FeedbackSelector({ label, feedback, onChange }) {
9
+ return (
10
+ <div data-testid="feedback-selector">
11
+ <label>{label}</label>
12
+ <select
13
+ aria-label={label}
14
+ value={feedback.type}
15
+ onChange={(e) => onChange({ ...feedback, type: e.target.value })}
16
+ >
17
+ <option value="default">Default</option>
18
+ <option value="custom">Custom</option>
19
+ <option value="none">None</option>
20
+ </select>
21
+ </div>
22
+ );
23
+ };
24
+ });
25
+
26
+ describe('FeedbackConfig', () => {
27
+ const onChange = jest.fn();
28
+
29
+ beforeEach(() => {
30
+ onChange.mockClear();
31
+ });
32
+
33
+ describe('rendering', () => {
34
+ it('renders with default feedback types', () => {
35
+ const feedback = buildDefaults();
36
+
37
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
38
+
39
+ expect(screen.getByText('Feedback')).toBeInTheDocument();
40
+ expect(screen.getByText('If correct, show')).toBeInTheDocument();
41
+ expect(screen.getByText('If partially correct, show')).toBeInTheDocument();
42
+ expect(screen.getByText('If incorrect, show')).toBeInTheDocument();
43
+ });
44
+
45
+ it('renders all three feedback selectors by default', () => {
46
+ const feedback = buildDefaults();
47
+
48
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
49
+
50
+ const selectors = screen.getAllByTestId('feedback-selector');
51
+ expect(selectors).toHaveLength(3);
52
+ });
53
+
54
+ it('does not render partial feedback selector when allowPartial is false', () => {
55
+ const feedback = buildDefaults();
56
+
57
+ render(<FeedbackConfig allowPartial={false} feedback={feedback} onChange={onChange} />);
58
+
59
+ expect(screen.getByText('If correct, show')).toBeInTheDocument();
60
+ expect(screen.queryByText('If partially correct, show')).not.toBeInTheDocument();
61
+ expect(screen.getByText('If incorrect, show')).toBeInTheDocument();
62
+
63
+ const selectors = screen.getAllByTestId('feedback-selector');
64
+ expect(selectors).toHaveLength(2);
65
+ });
66
+ });
67
+
68
+ describe('user interactions', () => {
69
+ it('calls onChange when correct feedback type changes', async () => {
70
+ const user = userEvent.setup();
71
+ const feedback = buildDefaults();
72
+
73
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
74
+
75
+ const correctSelect = screen.getByLabelText('If correct, show');
76
+ await user.selectOptions(correctSelect, 'custom');
77
+
78
+ expect(onChange).toHaveBeenCalledWith(
79
+ expect.objectContaining({
80
+ correct: expect.objectContaining({ type: 'custom' }),
81
+ }),
82
+ );
83
+ });
84
+
85
+ it('calls onChange when incorrect feedback type changes', async () => {
86
+ const user = userEvent.setup();
87
+ const feedback = buildDefaults();
88
+
89
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
90
+
91
+ const incorrectSelect = screen.getByLabelText('If incorrect, show');
92
+ await user.selectOptions(incorrectSelect, 'none');
93
+
94
+ expect(onChange).toHaveBeenCalledWith(
95
+ expect.objectContaining({
96
+ incorrect: expect.objectContaining({ type: 'none' }),
97
+ }),
98
+ );
99
+ });
100
+
101
+ it('calls onChange when partial feedback type changes', async () => {
102
+ const user = userEvent.setup();
103
+ const feedback = buildDefaults();
104
+
105
+ render(<FeedbackConfig feedback={feedback} onChange={onChange} />);
106
+
107
+ const partialSelect = screen.getByLabelText('If partially correct, show');
108
+ await user.selectOptions(partialSelect, 'custom');
109
+
110
+ expect(onChange).toHaveBeenCalledWith(
111
+ expect.objectContaining({
112
+ partial: expect.objectContaining({ type: 'custom' }),
113
+ }),
114
+ );
115
+ });
116
+ });
117
+
118
+ describe('buildDefaults helper', () => {
119
+ it('returns default feedback configuration', () => {
120
+ const defaults = buildDefaults();
121
+
122
+ expect(defaults).toEqual({
123
+ correct: { type: 'default', default: 'Correct' },
124
+ incorrect: { type: 'default', default: 'Incorrect' },
125
+ partial: { type: 'default', default: 'Nearly' },
126
+ });
127
+ });
128
+
129
+ it('merges custom values with defaults', () => {
130
+ const defaults = buildDefaults({
131
+ correct: { type: 'custom', custom: 'Great job!' },
132
+ });
133
+
134
+ expect(defaults).toEqual({
135
+ correct: { type: 'custom', default: 'Correct', custom: 'Great job!' },
136
+ incorrect: { type: 'default', default: 'Incorrect' },
137
+ partial: { type: 'default', default: 'Nearly' },
138
+ });
139
+ });
140
+ });
141
+ });