@pie-lib/config-ui 12.0.0-beta.5 → 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 (116) hide show
  1. package/CHANGELOG.json +8 -1653
  2. package/CHANGELOG.md +345 -4
  3. package/LICENSE.md +5 -0
  4. package/NEXT.CHANGELOG.json +1 -0
  5. package/lib/alert-dialog.js +40 -10
  6. package/lib/alert-dialog.js.map +1 -1
  7. package/lib/checkbox.js +58 -48
  8. package/lib/checkbox.js.map +1 -1
  9. package/lib/choice-configuration/feedback-menu.js +24 -26
  10. package/lib/choice-configuration/feedback-menu.js.map +1 -1
  11. package/lib/choice-configuration/index.js +182 -185
  12. package/lib/choice-configuration/index.js.map +1 -1
  13. package/lib/choice-utils.js +5 -7
  14. package/lib/choice-utils.js.map +1 -1
  15. package/lib/feedback-config/feedback-selector.js +69 -73
  16. package/lib/feedback-config/feedback-selector.js.map +1 -1
  17. package/lib/feedback-config/group.js +22 -25
  18. package/lib/feedback-config/group.js.map +1 -1
  19. package/lib/feedback-config/index.js +41 -44
  20. package/lib/feedback-config/index.js.map +1 -1
  21. package/lib/form-section.js +31 -25
  22. package/lib/form-section.js.map +1 -1
  23. package/lib/help.js +37 -47
  24. package/lib/help.js.map +1 -1
  25. package/lib/index.js +1 -2
  26. package/lib/index.js.map +1 -1
  27. package/lib/input.js +12 -17
  28. package/lib/input.js.map +1 -1
  29. package/lib/inputs.js +58 -67
  30. package/lib/inputs.js.map +1 -1
  31. package/lib/langs.js +56 -70
  32. package/lib/langs.js.map +1 -1
  33. package/lib/layout/config-layout.js +78 -47
  34. package/lib/layout/config-layout.js.map +1 -1
  35. package/lib/layout/index.js.map +1 -1
  36. package/lib/layout/layout-contents.js +58 -60
  37. package/lib/layout/layout-contents.js.map +1 -1
  38. package/lib/layout/settings-box.js +25 -33
  39. package/lib/layout/settings-box.js.map +1 -1
  40. package/lib/mui-box/index.js +41 -50
  41. package/lib/mui-box/index.js.map +1 -1
  42. package/lib/number-text-field-custom.js +151 -89
  43. package/lib/number-text-field-custom.js.map +1 -1
  44. package/lib/number-text-field.js +74 -63
  45. package/lib/number-text-field.js.map +1 -1
  46. package/lib/radio-with-label.js +30 -16
  47. package/lib/radio-with-label.js.map +1 -1
  48. package/lib/settings/display-size.js +16 -20
  49. package/lib/settings/display-size.js.map +1 -1
  50. package/lib/settings/index.js +13 -19
  51. package/lib/settings/index.js.map +1 -1
  52. package/lib/settings/panel.js +140 -141
  53. package/lib/settings/panel.js.map +1 -1
  54. package/lib/settings/settings-radio-label.js +29 -16
  55. package/lib/settings/settings-radio-label.js.map +1 -1
  56. package/lib/settings/toggle.js +39 -25
  57. package/lib/settings/toggle.js.map +1 -1
  58. package/lib/tabs/index.js +18 -30
  59. package/lib/tabs/index.js.map +1 -1
  60. package/lib/tags-input/index.js +49 -61
  61. package/lib/tags-input/index.js.map +1 -1
  62. package/lib/two-choice.js +33 -43
  63. package/lib/two-choice.js.map +1 -1
  64. package/lib/with-stateful-model.js +8 -12
  65. package/lib/with-stateful-model.js.map +1 -1
  66. package/package.json +22 -11
  67. package/src/__tests__/alert-dialog.test.jsx +283 -0
  68. package/src/__tests__/checkbox.test.jsx +249 -0
  69. package/src/__tests__/choice-utils.test.js +12 -0
  70. package/src/__tests__/form-section.test.jsx +334 -0
  71. package/src/__tests__/help.test.jsx +184 -0
  72. package/src/__tests__/input.test.jsx +192 -0
  73. package/src/__tests__/langs.test.jsx +457 -0
  74. package/src/__tests__/number-text-field-custom.test.jsx +438 -0
  75. package/src/__tests__/number-text-field.test.jsx +341 -0
  76. package/src/__tests__/radio-with-label.test.jsx +259 -0
  77. package/src/__tests__/settings-panel.test.js +187 -0
  78. package/src/__tests__/settings.test.jsx +515 -0
  79. package/src/__tests__/tabs.test.jsx +193 -0
  80. package/src/__tests__/two-choice.test.js +110 -0
  81. package/src/__tests__/with-stateful-model.test.jsx +145 -0
  82. package/src/alert-dialog.jsx +30 -8
  83. package/src/checkbox.jsx +43 -37
  84. package/src/choice-configuration/__tests__/feedback-menu.test.jsx +163 -0
  85. package/src/choice-configuration/__tests__/index.test.jsx +234 -0
  86. package/src/choice-configuration/feedback-menu.jsx +6 -6
  87. package/src/choice-configuration/index.jsx +208 -192
  88. package/src/feedback-config/__tests__/feedback-config.test.jsx +141 -0
  89. package/src/feedback-config/__tests__/feedback-selector.test.jsx +107 -0
  90. package/src/feedback-config/feedback-selector.jsx +52 -53
  91. package/src/feedback-config/group.jsx +21 -22
  92. package/src/feedback-config/index.jsx +27 -29
  93. package/src/form-section.jsx +26 -18
  94. package/src/help.jsx +20 -28
  95. package/src/input.jsx +1 -1
  96. package/src/inputs.jsx +35 -44
  97. package/src/langs.jsx +41 -46
  98. package/src/layout/__tests__/config.layout.test.jsx +59 -0
  99. package/src/layout/__tests__/layout-content.test.jsx +3 -0
  100. package/src/layout/config-layout.jsx +53 -23
  101. package/src/layout/layout-contents.jsx +38 -40
  102. package/src/layout/settings-box.jsx +16 -19
  103. package/src/mui-box/index.jsx +35 -43
  104. package/src/number-text-field-custom.jsx +117 -65
  105. package/src/number-text-field.jsx +51 -34
  106. package/src/radio-with-label.jsx +26 -10
  107. package/src/settings/display-size.jsx +12 -11
  108. package/src/settings/index.js +2 -1
  109. package/src/settings/panel.jsx +101 -92
  110. package/src/settings/settings-radio-label.jsx +26 -10
  111. package/src/settings/toggle.jsx +37 -18
  112. package/src/tabs/index.jsx +8 -8
  113. package/src/tags-input/__tests__/index.test.jsx +113 -0
  114. package/src/tags-input/index.jsx +35 -38
  115. package/src/two-choice.jsx +15 -19
  116. package/README.md +0 -12
@@ -0,0 +1,163 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import FeedbackMenu from '../feedback-menu';
5
+
6
+ describe('feedback-menu', () => {
7
+ const onChange = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ onChange.mockClear();
11
+ });
12
+
13
+ describe('rendering', () => {
14
+ it('renders feedback menu with default options', () => {
15
+ const { container } = render(<FeedbackMenu onChange={onChange} classes={{}} />);
16
+ expect(container.firstChild).toBeInTheDocument();
17
+ });
18
+
19
+ it('renders menu with feedback type selector', () => {
20
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />);
21
+ // Menu button should be in the document
22
+ expect(screen.getByRole('button')).toBeInTheDocument();
23
+ });
24
+
25
+ it('renders with no feedback type', () => {
26
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'none' }} classes={{}} />);
27
+ expect(screen.getByRole('button')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders with custom feedback type', () => {
31
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'custom' }} classes={{}} />);
32
+ expect(screen.getByRole('button')).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders feedback icon button', () => {
36
+ render(<FeedbackMenu onChange={onChange} classes={{}} />);
37
+ const button = screen.getByRole('button');
38
+ expect(button).toBeInTheDocument();
39
+ });
40
+ });
41
+
42
+ describe('user interactions', () => {
43
+ it('opens menu when button is clicked', async () => {
44
+ const user = userEvent.setup();
45
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />);
46
+
47
+ const button = screen.getByRole('button');
48
+ await user.click(button);
49
+
50
+ // Menu items should appear
51
+ expect(screen.getByText('No Feedback')).toBeInTheDocument();
52
+ expect(screen.getByText('Default')).toBeInTheDocument();
53
+ expect(screen.getByText('Custom')).toBeInTheDocument();
54
+ });
55
+
56
+ it('calls onChange with "none" when No Feedback is selected', async () => {
57
+ const user = userEvent.setup();
58
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />);
59
+
60
+ const button = screen.getByRole('button');
61
+ await user.click(button);
62
+
63
+ const noFeedbackOption = screen.getByText('No Feedback');
64
+ await user.click(noFeedbackOption);
65
+
66
+ expect(onChange).toHaveBeenCalledWith('none');
67
+ });
68
+
69
+ it('calls onChange with "default" when Default is selected', async () => {
70
+ const user = userEvent.setup();
71
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'none' }} classes={{}} />);
72
+
73
+ const button = screen.getByRole('button');
74
+ await user.click(button);
75
+
76
+ const defaultOption = screen.getByText('Default');
77
+ await user.click(defaultOption);
78
+
79
+ expect(onChange).toHaveBeenCalledWith('default');
80
+ });
81
+
82
+ it('calls onChange with "custom" when Custom is selected', async () => {
83
+ const user = userEvent.setup();
84
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'none' }} classes={{}} />);
85
+
86
+ const button = screen.getByRole('button');
87
+ await user.click(button);
88
+
89
+ const customOption = screen.getByText('Custom');
90
+ await user.click(customOption);
91
+
92
+ expect(onChange).toHaveBeenCalledWith('custom');
93
+ });
94
+
95
+ it('closes menu after selection', async () => {
96
+ const user = userEvent.setup();
97
+ render(<FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />);
98
+
99
+ const button = screen.getByRole('button');
100
+ await user.click(button);
101
+
102
+ expect(screen.getByText('No Feedback')).toBeInTheDocument();
103
+
104
+ const noFeedbackOption = screen.getByText('No Feedback');
105
+ await user.click(noFeedbackOption);
106
+
107
+ // Menu should close after selection
108
+ expect(onChange).toHaveBeenCalledWith('none');
109
+ });
110
+ });
111
+
112
+ describe('feedback type indicators', () => {
113
+ it('shows correct icon color for default feedback', () => {
114
+ const { container } = render(
115
+ <FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />
116
+ );
117
+ // Icon should be present with primary color
118
+ expect(container.querySelector('svg')).toBeInTheDocument();
119
+ });
120
+
121
+ it('shows correct icon color for custom feedback', () => {
122
+ const { container } = render(
123
+ <FeedbackMenu onChange={onChange} value={{ type: 'custom' }} classes={{}} />
124
+ );
125
+ // Icon should be present with primary color
126
+ expect(container.querySelector('svg')).toBeInTheDocument();
127
+ });
128
+
129
+ it('shows correct icon color for disabled feedback', () => {
130
+ const { container } = render(
131
+ <FeedbackMenu onChange={onChange} value={{ type: 'none' }} classes={{}} />
132
+ );
133
+ // Icon should be present with disabled color
134
+ expect(container.querySelector('svg')).toBeInTheDocument();
135
+ });
136
+ });
137
+
138
+ describe('aria labels', () => {
139
+ it('has aria-label on button for default feedback', () => {
140
+ render(
141
+ <FeedbackMenu onChange={onChange} value={{ type: 'default' }} classes={{}} />
142
+ );
143
+ const button = screen.getByRole('button', { name: /Default Feedback/i });
144
+ expect(button).toBeInTheDocument();
145
+ });
146
+
147
+ it('has aria-label on button for custom feedback', () => {
148
+ render(
149
+ <FeedbackMenu onChange={onChange} value={{ type: 'custom' }} classes={{}} />
150
+ );
151
+ const button = screen.getByRole('button', { name: /Custom Feedback/i });
152
+ expect(button).toBeInTheDocument();
153
+ });
154
+
155
+ it('has aria-label on button for disabled feedback', () => {
156
+ render(
157
+ <FeedbackMenu onChange={onChange} value={{ type: 'none' }} classes={{}} />
158
+ );
159
+ const button = screen.getByRole('button', { name: /Feedback disabled/i });
160
+ expect(button).toBeInTheDocument();
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,234 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { ChoiceConfiguration } from '../index';
5
+
6
+ jest.mock('@pie-lib/editable-html', () => ({
7
+ __esModule: true,
8
+ default: ({ markup, onChange, disabled }) => (
9
+ <textarea
10
+ data-testid="editable-html"
11
+ defaultValue={markup || ''}
12
+ onChange={(e) => onChange(e.target.value)}
13
+ disabled={disabled}
14
+ />
15
+ ),
16
+ }));
17
+
18
+ const defaultFeedback = {
19
+ correct: 'Correct',
20
+ incorrect: 'Incorrect',
21
+ };
22
+
23
+ const data = {
24
+ correct: true,
25
+ value: 'foo',
26
+ label: 'Foo',
27
+ feedback: {
28
+ type: 'custom',
29
+ },
30
+ };
31
+
32
+ const classes = {
33
+ choiceConfiguration: 'choiceConfiguration',
34
+ };
35
+
36
+ describe('ChoiceConfiguration', () => {
37
+ const onChange = jest.fn();
38
+
39
+ beforeEach(() => {
40
+ onChange.mockClear();
41
+ });
42
+
43
+ describe('rendering', () => {
44
+ it('renders correctly with default props', () => {
45
+ const { container } = render(
46
+ <ChoiceConfiguration classes={classes} defaultFeedback={defaultFeedback} data={data} onChange={onChange} />,
47
+ );
48
+ expect(container.firstChild).toBeInTheDocument();
49
+ });
50
+
51
+ it('renders with checked state', () => {
52
+ render(
53
+ <ChoiceConfiguration
54
+ classes={classes}
55
+ defaultFeedback={defaultFeedback}
56
+ data={data}
57
+ onChange={onChange}
58
+ mode="checkbox"
59
+ />,
60
+ );
61
+ const checkbox = screen.getByRole('checkbox');
62
+ expect(checkbox).toBeChecked();
63
+ });
64
+
65
+ it('renders without feedback when allowFeedBack is false', () => {
66
+ render(
67
+ <ChoiceConfiguration
68
+ allowFeedBack={false}
69
+ classes={classes}
70
+ defaultFeedback={defaultFeedback}
71
+ data={data}
72
+ onChange={onChange}
73
+ />,
74
+ );
75
+ expect(screen.queryByRole('button', { name: /feedback/i })).not.toBeInTheDocument();
76
+ });
77
+
78
+ it('renders without delete button when allowDelete is false', () => {
79
+ render(
80
+ <ChoiceConfiguration
81
+ allowDelete={false}
82
+ classes={classes}
83
+ defaultFeedback={defaultFeedback}
84
+ data={data}
85
+ onChange={onChange}
86
+ />,
87
+ );
88
+ expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ describe('user interactions', () => {
93
+ it('calls onChange when label is edited', async () => {
94
+ const user = userEvent.setup();
95
+ render(
96
+ <ChoiceConfiguration classes={classes} defaultFeedback={defaultFeedback} data={data} onChange={onChange} />,
97
+ );
98
+
99
+ const editableHtmlElements = screen.getAllByTestId('editable-html');
100
+ const editableHtml = editableHtmlElements[0];
101
+ await user.clear(editableHtml);
102
+ await user.type(editableHtml, 'new label');
103
+
104
+ expect(onChange).toHaveBeenCalled();
105
+ expect(onChange).toHaveBeenCalledWith(
106
+ expect.objectContaining({
107
+ label: expect.stringContaining('new label'),
108
+ }),
109
+ );
110
+ });
111
+
112
+ it('calls onChange when checkbox is toggled', async () => {
113
+ const user = userEvent.setup();
114
+ render(
115
+ <ChoiceConfiguration
116
+ classes={classes}
117
+ defaultFeedback={defaultFeedback}
118
+ data={{ ...data, correct: false }}
119
+ onChange={onChange}
120
+ mode="checkbox"
121
+ />,
122
+ );
123
+
124
+ const checkbox = screen.getByRole('checkbox');
125
+ await user.click(checkbox);
126
+
127
+ expect(onChange).toHaveBeenCalledWith(
128
+ expect.objectContaining({
129
+ correct: true,
130
+ }),
131
+ );
132
+ });
133
+ });
134
+
135
+ describe('prop variations', () => {
136
+ it('renders with radio mode instead of checkbox', () => {
137
+ render(
138
+ <ChoiceConfiguration
139
+ classes={classes}
140
+ defaultFeedback={defaultFeedback}
141
+ data={data}
142
+ onChange={onChange}
143
+ mode="radio"
144
+ />,
145
+ );
146
+ const radio = screen.getByRole('radio');
147
+ expect(radio).toBeInTheDocument();
148
+ });
149
+
150
+ it('renders with disabled state', () => {
151
+ render(
152
+ <ChoiceConfiguration
153
+ classes={classes}
154
+ defaultFeedback={defaultFeedback}
155
+ data={data}
156
+ onChange={onChange}
157
+ disabled={true}
158
+ mode="checkbox"
159
+ />,
160
+ );
161
+ const checkbox = screen.getByRole('checkbox');
162
+ expect(checkbox).toBeInTheDocument();
163
+ });
164
+
165
+ it('renders with custom feedback type', () => {
166
+ render(
167
+ <ChoiceConfiguration
168
+ classes={classes}
169
+ defaultFeedback={defaultFeedback}
170
+ data={{ ...data, feedback: { type: 'custom' } }}
171
+ onChange={onChange}
172
+ mode="checkbox"
173
+ />,
174
+ );
175
+ const editableElements = screen.getAllByTestId('editable-html');
176
+ expect(editableElements.length).toBeGreaterThan(0);
177
+ });
178
+
179
+ it('renders with incorrect answer', () => {
180
+ render(
181
+ <ChoiceConfiguration
182
+ classes={classes}
183
+ defaultFeedback={defaultFeedback}
184
+ data={{ ...data, correct: false }}
185
+ onChange={onChange}
186
+ mode="checkbox"
187
+ />,
188
+ );
189
+ const checkbox = screen.getByRole('checkbox');
190
+ expect(checkbox).not.toBeChecked();
191
+ });
192
+ });
193
+
194
+ describe('edge cases', () => {
195
+ it('handles data with empty label', () => {
196
+ render(
197
+ <ChoiceConfiguration
198
+ classes={classes}
199
+ defaultFeedback={defaultFeedback}
200
+ data={{ ...data, label: '' }}
201
+ onChange={onChange}
202
+ />,
203
+ );
204
+ const editableElements = screen.getAllByTestId('editable-html');
205
+ expect(editableElements.length).toBeGreaterThan(0);
206
+ });
207
+
208
+ it('handles multiple feedback types', () => {
209
+ const { rerender } = render(
210
+ <ChoiceConfiguration
211
+ classes={classes}
212
+ defaultFeedback={defaultFeedback}
213
+ data={{ ...data, feedback: { type: 'default' } }}
214
+ onChange={onChange}
215
+ />,
216
+ );
217
+
218
+ let editableElements = screen.getAllByTestId('editable-html');
219
+ expect(editableElements.length).toBeGreaterThan(0);
220
+
221
+ rerender(
222
+ <ChoiceConfiguration
223
+ classes={classes}
224
+ defaultFeedback={defaultFeedback}
225
+ data={{ ...data, feedback: { type: 'custom' } }}
226
+ onChange={onChange}
227
+ />,
228
+ );
229
+
230
+ editableElements = screen.getAllByTestId('editable-html');
231
+ expect(editableElements.length).toBeGreaterThan(0);
232
+ });
233
+ });
234
+ });
@@ -1,7 +1,7 @@
1
- import Menu from '@material-ui/core/Menu';
2
- import MenuItem from '@material-ui/core/MenuItem';
3
- import ActionFeedback from '@material-ui/icons/Feedback';
4
- import IconButton from '@material-ui/core/IconButton';
1
+ import Menu from '@mui/material/Menu';
2
+ import MenuItem from '@mui/material/MenuItem';
3
+ import ActionFeedback from '@mui/icons-material/Feedback';
4
+ import IconButton from '@mui/material/IconButton';
5
5
  import PropTypes from 'prop-types';
6
6
  import React from 'react';
7
7
 
@@ -40,7 +40,7 @@ export class IconMenu extends React.Component {
40
40
  return (
41
41
  <div>
42
42
  <div onClick={this.handleClick}>{this.props.iconButtonElement}</div>
43
- <Menu id="simple-menu" anchorEl={this.state.anchorEl} open={this.state.open} onClose={this.handleRequestClose}>
43
+ <Menu id="simple-menu" anchorEl={this.state.anchorEl} open={this.state.open} onClose={this.handleRequestClose} transitionDuration={{ enter: 225, exit: 195 }}>
44
44
  {keys.map((k, index) => (
45
45
  <MenuItem key={index} onClick={handleMenuClick(k)}>
46
46
  {opts[k]}
@@ -70,7 +70,7 @@ export default class FeedbackMenu extends React.Component {
70
70
  const tooltip = t === 'custom' ? 'Custom Feedback' : t === 'default' ? 'Default Feedback' : 'Feedback disabled';
71
71
 
72
72
  const icon = (
73
- <IconButton className={classes.icon} aria-label={tooltip}>
73
+ <IconButton className={classes.icon} aria-label={tooltip} size="large">
74
74
  <ActionFeedback color={iconColor} />
75
75
  </IconButton>
76
76
  );