@pareto-engineering/design-system 5.0.5 → 5.1.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.
Files changed (96) hide show
  1. package/dist/cjs/a/Charts/AreaChart/AreaChart.js +3 -13
  2. package/dist/cjs/a/Charts/BarChart/BarChart.js +6 -4
  3. package/dist/cjs/a/Charts/Common/CustomLegend/CustomLegend.js +26 -7
  4. package/dist/cjs/a/Charts/Common/CustomLegend/styles.scss +41 -14
  5. package/dist/cjs/a/Charts/Common/CustomTooltipContent/CustomTooltipContent.js +18 -7
  6. package/dist/cjs/a/Charts/Common/YLabelsDropDown/YlabelsDropDown.js +3 -4
  7. package/dist/cjs/a/Charts/Common/YLabelsDropDown/styles.scss +7 -6
  8. package/dist/cjs/a/Charts/PieChart/PieChart.js +99 -0
  9. package/dist/cjs/a/Charts/PieChart/index.js +13 -0
  10. package/dist/cjs/a/Charts/PieChart/styles.scss +48 -0
  11. package/dist/cjs/a/Charts/index.js +8 -1
  12. package/dist/cjs/a/Notification/styles.scss +17 -5
  13. package/dist/cjs/a/index.js +6 -0
  14. package/dist/cjs/f/FormInput/FormInput.js +1 -1
  15. package/dist/cjs/f/fields/EditorInput/EditorInput.js +7 -3
  16. package/dist/cjs/f/fields/EditorInput/common/ExposePlainTextPlugin.js +40 -0
  17. package/dist/cjs/f/fields/EditorInput/common/ToolbarPlugin/ToolbarPlugin.js +1 -1
  18. package/dist/cjs/f/fields/EditorInput/common/index.js +7 -0
  19. package/dist/cjs/f/fields/LatexPreviewInput/LatexPreviewInput.js +1 -1
  20. package/dist/cjs/f/fields/LatexPreviewInput/styles.scss +1 -0
  21. package/dist/cjs/f/fields/TextareaInput/TextareaInput.js +4 -2
  22. package/dist/cjs/g/ExpandableLexicalPreview/ExpandableLexicalPreview.js +4 -2
  23. package/dist/cjs/g/ExpandableLexicalPreview/styles.scss +0 -1
  24. package/dist/cjs/g/FormBuilder/common/Builder/common/InputBuilder/InputBuilder.js +27 -1
  25. package/dist/cjs/g/FormBuilder/common/Builder/common/InputBuilder/styles.scss +15 -0
  26. package/dist/cjs/g/FormBuilder/common/Builder/common/Section/Section.js +6 -2
  27. package/dist/cjs/g/FormBuilder/common/Renderer/Renderer.js +6 -0
  28. package/dist/cjs/utils/applyCharacterLimit.js +75 -0
  29. package/dist/cjs/utils/formatting.js +10 -2
  30. package/dist/cjs/utils/index.js +14 -1
  31. package/dist/es/a/Charts/AreaChart/AreaChart.js +3 -13
  32. package/dist/es/a/Charts/BarChart/BarChart.js +6 -4
  33. package/dist/es/a/Charts/Common/CustomLegend/CustomLegend.js +38 -21
  34. package/dist/es/a/Charts/Common/CustomLegend/styles.scss +41 -14
  35. package/dist/es/a/Charts/Common/CustomTooltipContent/CustomTooltipContent.js +19 -8
  36. package/dist/es/a/Charts/Common/YLabelsDropDown/YlabelsDropDown.js +3 -5
  37. package/dist/es/a/Charts/Common/YLabelsDropDown/styles.scss +7 -6
  38. package/dist/es/a/Charts/PieChart/PieChart.js +89 -0
  39. package/dist/es/a/Charts/PieChart/index.js +1 -0
  40. package/dist/es/a/Charts/PieChart/styles.scss +48 -0
  41. package/dist/es/a/Charts/index.js +2 -1
  42. package/dist/es/a/Notification/styles.scss +17 -5
  43. package/dist/es/a/index.js +1 -1
  44. package/dist/es/f/FormInput/FormInput.js +1 -1
  45. package/dist/es/f/fields/EditorInput/EditorInput.js +8 -4
  46. package/dist/es/f/fields/EditorInput/common/ExposePlainTextPlugin.js +32 -0
  47. package/dist/es/f/fields/EditorInput/common/ToolbarPlugin/ToolbarPlugin.js +1 -1
  48. package/dist/es/f/fields/EditorInput/common/index.js +2 -1
  49. package/dist/es/f/fields/LatexPreviewInput/LatexPreviewInput.js +1 -1
  50. package/dist/es/f/fields/LatexPreviewInput/styles.scss +1 -0
  51. package/dist/es/f/fields/TextareaInput/TextareaInput.js +4 -2
  52. package/dist/es/g/ExpandableLexicalPreview/ExpandableLexicalPreview.js +4 -2
  53. package/dist/es/g/ExpandableLexicalPreview/styles.scss +0 -1
  54. package/dist/es/g/FormBuilder/common/Builder/common/InputBuilder/InputBuilder.js +27 -1
  55. package/dist/es/g/FormBuilder/common/Builder/common/InputBuilder/styles.scss +15 -0
  56. package/dist/es/g/FormBuilder/common/Builder/common/Section/Section.js +6 -2
  57. package/dist/es/g/FormBuilder/common/Renderer/Renderer.js +6 -0
  58. package/dist/es/utils/applyCharacterLimit.js +67 -0
  59. package/dist/es/utils/formatting.js +7 -0
  60. package/dist/es/utils/index.js +2 -1
  61. package/package.json +3 -3
  62. package/src/stories/a/AreaChart.stories.jsx +1 -1
  63. package/src/stories/a/BarChart.stories.jsx +1 -1
  64. package/src/stories/a/PieChart.stories.jsx +53 -0
  65. package/src/ui/a/Charts/AreaChart/AreaChart.jsx +8 -14
  66. package/src/ui/a/Charts/BarChart/BarChart.jsx +4 -2
  67. package/src/ui/a/Charts/Common/CustomLegend/CustomLegend.jsx +54 -29
  68. package/src/ui/a/Charts/Common/CustomLegend/styles.scss +41 -14
  69. package/src/ui/a/Charts/Common/CustomTooltipContent/CustomTooltipContent.jsx +25 -13
  70. package/src/ui/a/Charts/Common/YLabelsDropDown/YlabelsDropDown.jsx +4 -4
  71. package/src/ui/a/Charts/Common/YLabelsDropDown/styles.scss +7 -6
  72. package/src/ui/a/Charts/PieChart/PieChart.jsx +125 -0
  73. package/src/ui/a/Charts/PieChart/index.js +1 -0
  74. package/src/ui/a/Charts/PieChart/styles.scss +48 -0
  75. package/src/ui/a/Charts/index.js +1 -0
  76. package/src/ui/a/Notification/styles.scss +17 -5
  77. package/src/ui/a/index.js +1 -1
  78. package/src/ui/f/FormInput/FormInput.jsx +1 -0
  79. package/src/ui/f/fields/EditorInput/EditorInput.jsx +24 -9
  80. package/src/ui/f/fields/EditorInput/common/ExposePlainTextPlugin.jsx +42 -0
  81. package/src/ui/f/fields/EditorInput/common/ToolbarPlugin/ToolbarPlugin.jsx +1 -1
  82. package/src/ui/f/fields/EditorInput/common/index.js +1 -0
  83. package/src/ui/f/fields/LatexPreviewInput/LatexPreviewInput.jsx +1 -0
  84. package/src/ui/f/fields/LatexPreviewInput/styles.scss +1 -0
  85. package/src/ui/f/fields/TextareaInput/TextareaInput.jsx +2 -0
  86. package/src/ui/g/ExpandableLexicalPreview/ExpandableLexicalPreview.jsx +3 -3
  87. package/src/ui/g/ExpandableLexicalPreview/styles.scss +0 -1
  88. package/src/ui/g/FormBuilder/common/Builder/common/InputBuilder/InputBuilder.jsx +34 -0
  89. package/src/ui/g/FormBuilder/common/Builder/common/InputBuilder/styles.scss +15 -0
  90. package/src/ui/g/FormBuilder/common/Builder/common/Section/Section.jsx +10 -2
  91. package/src/ui/g/FormBuilder/common/Renderer/Renderer.jsx +5 -0
  92. package/src/ui/g/FormBuilder/common/Renderer/common/Section/Section.jsx +0 -1
  93. package/src/ui/utils/applyCharacterLimit.js +80 -0
  94. package/src/ui/utils/formatting.js +8 -0
  95. package/src/ui/utils/index.js +4 -1
  96. package/tests/__snapshots__/Storyshots.test.js.snap +1167 -447
@@ -0,0 +1,125 @@
1
+ import * as React from 'react'
2
+
3
+ import PropTypes from 'prop-types'
4
+
5
+ import {
6
+ PieChart as RechartsPieChart,
7
+ Pie,
8
+ Cell,
9
+ ResponsiveContainer,
10
+ Tooltip,
11
+ } from 'recharts'
12
+
13
+ import styleNames from '@pareto-engineering/bem/exports'
14
+
15
+ import { CustomLegend, CustomTooltipContent } from '../Common'
16
+
17
+ import './styles.scss'
18
+
19
+ const baseClassName = styleNames.base
20
+
21
+ const componentClassName = 'pie-chart'
22
+
23
+ const PieChart = ({
24
+ id,
25
+ className: userClassName,
26
+ data,
27
+ title,
28
+ valueKey,
29
+ labelKey,
30
+ colors,
31
+ height,
32
+ width,
33
+ innerRadius,
34
+ outerRadius,
35
+ }) => {
36
+ const total = data.reduce((sum, item) => sum + item[valueKey], 0)
37
+
38
+ const formattedData = data.map((item) => ({
39
+ ...item,
40
+ label :item[labelKey],
41
+ color :colors[data.indexOf(item)],
42
+ percentage:((item[valueKey] / total) * 100).toFixed(0),
43
+ }))
44
+
45
+ return (
46
+ <div
47
+ id={id}
48
+ className={[
49
+ baseClassName,
50
+ componentClassName,
51
+ userClassName,
52
+ ]
53
+ .filter((e) => e)
54
+ .join(' ')}
55
+ >
56
+ <div className="chart-header">
57
+ <h3>{title}</h3>
58
+ </div>
59
+ <div className="chart-content">
60
+ <ResponsiveContainer width={width} height={height}>
61
+ <RechartsPieChart>
62
+ <Pie
63
+ data={formattedData}
64
+ dataKey={valueKey}
65
+ nameKey={labelKey}
66
+ cx="50%"
67
+ cy="50%"
68
+ innerRadius={innerRadius}
69
+ outerRadius={outerRadius}
70
+ label={false}
71
+ paddingAngle={0}
72
+ >
73
+ {formattedData.map((entry, index) => (
74
+ <Cell
75
+ key={entry[labelKey]}
76
+ fill={colors[index]}
77
+ strokeWidth={0}
78
+ />
79
+ ))}
80
+ </Pie>
81
+ <Tooltip content={<CustomTooltipContent isDateValue={false} />} />
82
+ </RechartsPieChart>
83
+ </ResponsiveContainer>
84
+ <CustomLegend
85
+ colorsArray={colors}
86
+ yKeysArray={formattedData}
87
+ capitalizedLegend
88
+ orientation="vertical"
89
+ getLegendItemTitle={(entry) => entry[labelKey]}
90
+ getLegendItemSubtitle={(entry) => entry[valueKey]}
91
+ />
92
+ </div>
93
+ </div>
94
+ )
95
+ }
96
+
97
+ PieChart.propTypes = {
98
+ id :PropTypes.string,
99
+ className:PropTypes.string,
100
+ data :PropTypes.arrayOf(PropTypes.shape({
101
+ [PropTypes.string]:PropTypes.oneOfType([
102
+ PropTypes.string,
103
+ PropTypes.number,
104
+ ]),
105
+ })).isRequired,
106
+ title :PropTypes.string.isRequired,
107
+ valueKey :PropTypes.string.isRequired,
108
+ labelKey :PropTypes.string.isRequired,
109
+ colors :PropTypes.arrayOf(PropTypes.string).isRequired,
110
+ height :PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
111
+ width :PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
112
+ innerRadius:PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
113
+ outerRadius:PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
114
+ }
115
+
116
+ PieChart.defaultProps = {
117
+ id :undefined,
118
+ className :undefined,
119
+ width :'100%',
120
+ height :300,
121
+ innerRadius:'0%',
122
+ outerRadius:'100%',
123
+ }
124
+
125
+ export default PieChart
@@ -0,0 +1 @@
1
+ export { default as PieChart } from './PieChart'
@@ -0,0 +1,48 @@
1
+ @use "@pareto-engineering/bem";
2
+
3
+ $default-margin: 1rem;
4
+ $default-padding: 1rem;
5
+ $default-box-shadow: 0 .25rem .75rem var(--ui-lines);
6
+ $default-text-font-size: calc(var(--s-1) * 1rem);
7
+
8
+ .#{bem.$base} {
9
+ &.pie-chart {
10
+ background-color: var(--background-far);
11
+ border-radius: var(--theme-default-border-radius);
12
+ box-shadow: $default-box-shadow;
13
+ margin: $default-margin 0;
14
+ padding: $default-padding;
15
+
16
+ .chart-header {
17
+ align-items: center;
18
+ display: flex;
19
+ justify-content: space-between;
20
+ margin-bottom: $default-margin;
21
+
22
+ h3 {
23
+ color: var(--subtitle);
24
+ margin: calc($default-margin / 5);
25
+ text-align: left;
26
+ }
27
+ }
28
+
29
+ .chart-content {
30
+ align-items: flex-start;
31
+ display: flex;
32
+ }
33
+
34
+ .recharts-wrapper {
35
+ .recharts-surface {
36
+ .recharts-text {
37
+ fill: var(--soft-paragraph);
38
+ font-size: calc($default-text-font-size * .75);
39
+ }
40
+
41
+ .recharts-text.recharts-label {
42
+ fill: var(--paragraph);
43
+ font-size: $default-text-font-size;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
@@ -1,2 +1,3 @@
1
1
  export { AreaChart } from './AreaChart'
2
2
  export { BarChart } from './BarChart'
3
+ export { PieChart } from './PieChart'
@@ -4,6 +4,7 @@
4
4
  @use "@pareto-engineering/styles/src/mixins";
5
5
  @use "@pareto-engineering/styles/src/globals" as *;
6
6
 
7
+ $default-border: 1px solid var(--x);
7
8
  $default-padding: 1rem;
8
9
  $default-margin: 1rem;
9
10
  $default-border-radius: 1.5rem;
@@ -12,10 +13,11 @@ $default-height: var(--notification-height, 5rem);
12
13
 
13
14
  .#{bem.$base}.notification {
14
15
  align-items: center;
15
- background-color: var(--x);
16
+ background-color: var(--background-far);
17
+ border: $default-border;
16
18
  border-radius: $default-border-radius;
17
19
  bottom: 0;
18
- color: var(--on-x);
20
+ color: var(--paragraph);
19
21
  display: flex;
20
22
  justify-content: space-between;
21
23
  margin-bottom: $default-margin;
@@ -31,8 +33,16 @@ $default-height: var(--notification-height, 5rem);
31
33
  > .message-container {
32
34
  align-items: center;
33
35
  display: flex;
36
+ gap: $default-padding;
34
37
  overflow: auto;
35
38
 
39
+ > .icon {
40
+ background-color: var(--x);
41
+ border-radius: 50%;
42
+ color: var(--white);
43
+ padding: calc($default-padding / 2);
44
+ }
45
+
36
46
  > .message {
37
47
  font-size: calc(var(--s0) * 1rem);
38
48
  margin-left: calc($default-margin / 2);
@@ -44,12 +54,14 @@ $default-height: var(--notification-height, 5rem);
44
54
  }
45
55
 
46
56
  .#{bem.$base}.button {
57
+ background-color: transparent;
58
+ color: var(--paragraph);
47
59
  padding: calc($default-padding / 2);
48
60
 
49
- &:focus {
61
+ &:focus,
62
+ &:hover {
50
63
  background-color: transparent;
64
+ color: var(--hard-paragraph);
51
65
  }
52
66
  }
53
67
  }
54
-
55
-
package/src/ui/a/index.js CHANGED
@@ -30,4 +30,4 @@ export { ToggleSwitch } from './ToggleSwitch'
30
30
  export { XMLEditor } from './XMLEditor'
31
31
  export { DatePicker } from './DatePicker'
32
32
  export { Tooltip } from './Tooltip'
33
- export { AreaChart, BarChart } from './Charts'
33
+ export { AreaChart, BarChart, PieChart } from './Charts'
@@ -42,6 +42,7 @@ const FormInput = ({
42
42
  const newClassName = [
43
43
  className,
44
44
  componentClassName,
45
+ otherProps.hasCharacterLimit && otherProps.maxLength && `limit-character-count-${otherProps.maxLength}`,
45
46
  ].filter(Boolean).join(' ')
46
47
 
47
48
  if (type === 'textarea') {
@@ -26,7 +26,12 @@ import styleNames from '@pareto-engineering/bem/exports'
26
26
  // Local Definitions
27
27
 
28
28
  import { FormLabel, FormDescription } from '../../common'
29
- import { ToolbarPlugin, TreeViewPlugin, StopPropagationPlugin } from './common'
29
+ import {
30
+ ToolbarPlugin,
31
+ TreeViewPlugin,
32
+ StopPropagationPlugin,
33
+ ExposePlainTextPlugin,
34
+ } from './common'
30
35
 
31
36
  import './styles.scss'
32
37
 
@@ -60,6 +65,7 @@ const EditorInput = ({
60
65
  disabled,
61
66
  showDebugger,
62
67
  setEditorState,
68
+ setPlainTextKey,
63
69
  // ...otherProps
64
70
  }) => {
65
71
  const [field, ,] = useField(name)
@@ -157,14 +163,17 @@ const EditorInput = ({
157
163
  '--rows' :`${rows}em`,
158
164
  }}
159
165
  >
160
- <FormLabel
161
- name={name}
162
- color={labelColor}
163
- optional={optional}
164
- >
165
- {label}
166
- </FormLabel>
167
-
166
+ {
167
+ label && (
168
+ <FormLabel
169
+ name={name}
170
+ color={labelColor}
171
+ optional={optional}
172
+ >
173
+ {label}
174
+ </FormLabel>
175
+ )
176
+ }
168
177
  { !disabled && <ToolbarPlugin /> }
169
178
  <RichTextPlugin
170
179
  contentEditable={(
@@ -187,6 +196,12 @@ const EditorInput = ({
187
196
  <StopPropagationPlugin />
188
197
  <FormDescription className="s-1" description={description} name={name} />
189
198
  { showDebugger && <TreeViewPlugin /> }
199
+ {setPlainTextKey && (
200
+ <ExposePlainTextPlugin
201
+ setFieldValue={setFieldValue}
202
+ setPlainTextKey={setPlainTextKey}
203
+ />
204
+ )}
190
205
  </div>
191
206
  </LexicalComposer>
192
207
  )
@@ -0,0 +1,42 @@
1
+ import { useEffect } from 'react'
2
+
3
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
4
+
5
+ import {
6
+ $getRoot,
7
+ } from 'lexical'
8
+
9
+ import PropTypes from 'prop-types'
10
+
11
+ /**
12
+ * This is the component description
13
+ */
14
+ const ExposePlainTextPlugin = ({
15
+ setFieldValue,
16
+ setPlainTextKey,
17
+ }) => {
18
+ const [editor] = useLexicalComposerContext()
19
+
20
+ useEffect(() => (
21
+ editor.registerUpdateListener(({ editorState }) => {
22
+ editorState.read(() => {
23
+ const root = $getRoot()
24
+ const textContent = root.getTextContent()
25
+ setFieldValue(setPlainTextKey, textContent)
26
+ })
27
+ })
28
+ ), [editor])
29
+
30
+ return null
31
+ }
32
+
33
+ ExposePlainTextPlugin.propTypes = {
34
+ setFieldValue :PropTypes.func.isRequired,
35
+ setPlainTextKey:PropTypes.string.isRequired,
36
+ }
37
+
38
+ ExposePlainTextPlugin.defaultProps = {
39
+ //
40
+ }
41
+
42
+ export default ExposePlainTextPlugin
@@ -254,7 +254,7 @@ const ToolbarPlugin = ({
254
254
  baseClassName,
255
255
  componentClassName,
256
256
  userClassName,
257
- `x-${baseBgColor}`,
257
+ `x-${baseBgColor} b-x`,
258
258
  ]
259
259
  .filter((e) => e)
260
260
  .join(' ')}
@@ -4,3 +4,4 @@ export { default as StopPropagationPlugin } from './StopPropagationPlugin'
4
4
  export { ToolbarPlugin } from './ToolbarPlugin'
5
5
  export { ColorPicker } from './ColorPicker'
6
6
  export { FontSizeDropDown } from './FontSizeDropDown'
7
+ export { default as ExposePlainTextPlugin } from './ExposePlainTextPlugin'
@@ -44,6 +44,7 @@ const LatexPreviewInput = ({
44
44
  className={[
45
45
  baseClassName,
46
46
  componentClassName,
47
+ className,
47
48
  userClassName,
48
49
  'form-input',
49
50
  ]
@@ -11,6 +11,7 @@ $default-margin: 1em;
11
11
 
12
12
  > .preview-child {
13
13
  flex: 1;
14
+ position: relative;
14
15
  }
15
16
 
16
17
  > .latex-container {
@@ -38,6 +38,7 @@ const TextareaInput = ({
38
38
  placeholder,
39
39
  autoComplete,
40
40
  resize,
41
+ maxLength,
41
42
  // ...otherProps
42
43
  }) => {
43
44
  const [field] = useField({ name, validate })
@@ -73,6 +74,7 @@ const TextareaInput = ({
73
74
  rows={rows}
74
75
  disabled={disabled}
75
76
  autoComplete={autoComplete}
77
+ maxLength={maxLength}
76
78
  >
77
79
  {/* It was a dark and stormy night... */}
78
80
  </textarea>
@@ -37,6 +37,7 @@ const ExpandableLexicalPreview = ({
37
37
  onBlock,
38
38
  onOpen,
39
39
  header,
40
+ rows,
40
41
  // ...otherProps
41
42
  }) => {
42
43
  const [isCollapsed, setIsCollapsed] = useState(false)
@@ -54,9 +55,7 @@ const ExpandableLexicalPreview = ({
54
55
  <div
55
56
  id={id}
56
57
  className={[
57
-
58
58
  baseClassName,
59
-
60
59
  componentClassName,
61
60
  userClassName,
62
61
  `y-${color}`,
@@ -102,7 +101,8 @@ const ExpandableLexicalPreview = ({
102
101
  name={name}
103
102
  resize={resize}
104
103
  disabled
105
- id="the-bad-ass-input"
104
+ id={`${id}-editor-input`}
105
+ rows={rows}
106
106
  />
107
107
  <Button
108
108
  id={id}
@@ -10,7 +10,6 @@
10
10
  position: relative;
11
11
  width: 100%;
12
12
 
13
-
14
13
  &.collapsed {
15
14
  flex-direction: row;
16
15
  }
@@ -70,6 +70,8 @@ const InputBuilder = ({
70
70
  setFieldValue(`sections.${sectionIndex}.inputs.${inputIndex}.showSpecificFileTypes`, !input.showSpecificFileTypes)
71
71
  }
72
72
 
73
+ const textChoices = ['text', 'number', 'textarea', 'latex-preview-input']
74
+
73
75
  return (
74
76
  <div
75
77
  id={id}
@@ -179,6 +181,38 @@ const InputBuilder = ({
179
181
  />
180
182
  )}
181
183
  </div>
184
+ {textChoices.includes(input?.type) && (
185
+ <div className="character-limit-container">
186
+ <div className="is-required">
187
+ <span className="s0">
188
+ Limit number of characters permitted for this input
189
+ </span>
190
+ <ToggleSwitch
191
+ handleOnChange={() => {
192
+ setFieldValue(`sections.${sectionIndex}.inputs.${inputIndex}.hasCharacterLimit`, !input?.hasCharacterLimit)
193
+ if (!input?.hasCharacterLimit) {
194
+ setFieldValue(`sections.${sectionIndex}.inputs.${inputIndex}.maxLength`, '')
195
+ }
196
+ }}
197
+ checked={input?.hasCharacterLimit}
198
+ style={getToggleSwitchStyles(!input?.hasCharacterLimit)}
199
+ inputId={`sections_${sectionIndex}_inputs.${inputIndex}_character_limit_toggle`}
200
+ />
201
+ </div>
202
+ <div className="character-limit-input">
203
+ {input?.hasCharacterLimit && (
204
+ <TextInput
205
+ label="Enter the maximum number of characters permitted."
206
+ name={`sections.${sectionIndex}.inputs.${inputIndex}.maxLength`}
207
+ placeholder=""
208
+ type="number"
209
+ validate={integerAndGreaterThanZero}
210
+ value={input?.maxLength}
211
+ />
212
+ )}
213
+ </div>
214
+ </div>
215
+ )}
182
216
  {shouldRenderOptions && (
183
217
  <FieldArray name={`sections.${sectionIndex}.inputs.${inputIndex}.options`}>
184
218
  {({ push, remove }) => (
@@ -64,6 +64,21 @@ $default-list-width: var(--action-button-width, 18rem);
64
64
  margin-bottom: $default-margin;
65
65
  }
66
66
 
67
+ > .character-limit-container {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--gap);
71
+
72
+ > .is-required {
73
+ display: flex;
74
+ flex-direction: row;
75
+ }
76
+
77
+ > .character-limit-input {
78
+ flex-direction: column;
79
+ }
80
+ }
81
+
67
82
  > .input-options {
68
83
  display: flex;
69
84
  flex-direction: column;
@@ -62,7 +62,13 @@ const Section = ({
62
62
  const { inputs } = section
63
63
 
64
64
  useEffect(() => {
65
- const items = inputs.map((input, indx) => ({
65
+ const orderMap = new Map()
66
+ section.orderedInputDragIds.forEach(
67
+ (identifier, indx) => orderMap.set(identifier, indx),
68
+ )
69
+ const orderedInputs = inputs.sort((a, b) => orderMap.get(a.name) - orderMap.get(b.name))
70
+
71
+ const items = orderedInputs.map((input, indx) => ({
66
72
  Content:<InputBuilder
67
73
  key={input.name}
68
74
  sectionIndex={index}
@@ -75,6 +81,8 @@ const Section = ({
75
81
  setDraggableInputs(items)
76
82
  }, [inputs.length, index])
77
83
 
84
+ const dragAndDropKey = draggableInputs.map((e) => e.identifier).join(',')
85
+
78
86
  return (
79
87
  <div
80
88
  id={id}
@@ -128,7 +136,7 @@ const Section = ({
128
136
  const ids = reOrderedItems.map((e) => e.identifier)
129
137
  setFieldValue(`sections.${index}.orderedInputDragIds`, ids)
130
138
  }}
131
- key={draggableInputs.length}
139
+ key={dragAndDropKey}
132
140
  />
133
141
  <button
134
142
  type="button"
@@ -15,6 +15,7 @@ import './styles.scss'
15
15
 
16
16
  // Local Definitions
17
17
 
18
+ import { applyCharacterLimit } from 'ui/utils'
18
19
  import { Section } from './common'
19
20
 
20
21
  const baseClassName = styleNames.base
@@ -165,6 +166,10 @@ const Renderer = ({
165
166
  }
166
167
  }, [values])
167
168
 
169
+ useEffect(() => {
170
+ applyCharacterLimit({ setMaxLength: false })
171
+ }, [values])
172
+
168
173
  const hasErrors = Object.keys(errors).length > 0
169
174
  return (
170
175
  <Form>
@@ -60,7 +60,6 @@ const Section = ({
60
60
  />
61
61
  {inputs?.map((input, inputIndex) => {
62
62
  const isFileInput = input.type === 'file'
63
-
64
63
  return (
65
64
  <FormInput
66
65
  key={input.name}
@@ -0,0 +1,80 @@
1
+ export const applyCharacterLimit = (setMaxLength = true) => {
2
+ setTimeout(() => {
3
+ document.querySelectorAll("[class*='limit-character-count-']").forEach((parent) => {
4
+ const match = parent.className.match(/limit-character-count-(\d+)/)
5
+ if (!match) return
6
+
7
+ const maxLength = parseInt(match[1], 10)
8
+
9
+ const fields = parent.matches("input[type='text'], input[type='email'], input[type='password'], input[type='search'], textarea")
10
+ ? [parent]
11
+ : Array.from(parent.querySelectorAll("input[type='text'], input[type='email'], input[type='password'], input[type='search'], textarea"))
12
+
13
+ if (!fields.length) return
14
+
15
+ const characterCounterBaseClassName = 's-2 character-counter'
16
+ const characterCounterClassName = `${characterCounterBaseClassName} x-paragraph c-x`
17
+ const characterCounterWarningClassName = `${characterCounterBaseClassName} x-orange c-x`
18
+ const characterCounterErrorClassName = `${characterCounterBaseClassName} x-error c-x`
19
+
20
+ fields.forEach((field) => {
21
+ if (field.parentNode.querySelector('.character-counter')) return
22
+
23
+ if (setMaxLength) {
24
+ field.setAttribute('maxlength', maxLength)
25
+ }
26
+
27
+ const counter = document.createElement('span')
28
+ counter.className = characterCounterClassName
29
+ counter.style.position = 'absolute'
30
+ counter.style.right = '10px'
31
+ counter.style.bottom = '-20px'
32
+ counter.style.pointerEvents = 'none'
33
+
34
+ let wrapper
35
+ const { parentNode } = field
36
+ const computedStyle = window.getComputedStyle(parentNode)
37
+ if ((parentNode.style && parentNode.style.position === 'relative') || computedStyle.getPropertyValue('position') === 'relative') {
38
+ wrapper = parentNode
39
+ } else {
40
+ wrapper = document.createElement('div')
41
+ wrapper.style.position = 'relative'
42
+ wrapper.style.display = 'inline-block'
43
+ wrapper.style.width = '100%'
44
+ field.parentNode.insertBefore(wrapper, field)
45
+ wrapper.appendChild(field)
46
+ }
47
+
48
+ wrapper.appendChild(counter)
49
+
50
+ function updateCounter() {
51
+ if (field.value.length > maxLength) {
52
+ // eslint-disable-next-line no-param-reassign
53
+ field.value = field.value.substring(0, maxLength)
54
+ }
55
+
56
+ const { length } = field.value
57
+ counter.textContent = `${length}/${maxLength}`
58
+
59
+ if (length >= maxLength) {
60
+ counter.className = characterCounterErrorClassName
61
+ } else if (length >= maxLength * 0.9) {
62
+ counter.className = characterCounterWarningClassName
63
+ } else {
64
+ counter.className = characterCounterClassName
65
+ }
66
+ }
67
+
68
+ field.removeEventListener('input', updateCounter)
69
+ field.removeEventListener('paste', updateCounter)
70
+
71
+ field.addEventListener('input', updateCounter)
72
+ field.addEventListener('paste', () => {
73
+ setTimeout(updateCounter, 0)
74
+ })
75
+
76
+ updateCounter()
77
+ })
78
+ })
79
+ }, 100) // Small delay to ensure DOM is ready
80
+ }
@@ -46,6 +46,9 @@ export const formatTime = (seconds) => {
46
46
  if (minutes > 0 || (hours > 0 && remainingSeconds > 0)) parts.push(`${minutes}m`)
47
47
  if (remainingSeconds > 0) parts.push(`${remainingSeconds}s`)
48
48
 
49
+ // Cater for decimal seconds
50
+ if (parts.length === 0) return '0s'
51
+
49
52
  return parts.join(' ')
50
53
  }
51
54
 
@@ -123,3 +126,8 @@ export const formatDate = (input, format = DATE_FORMATS.HUMAN_READABLE) => {
123
126
  return 'Invalid Date'
124
127
  }
125
128
  }
129
+
130
+ export const snakeCaseToTitleCase = (word) => {
131
+ const result = word.replace(/([A-Z])/g, ' $1')
132
+ return result.charAt(0).toUpperCase() + result.slice(1)
133
+ }
@@ -1,2 +1,5 @@
1
1
  export { useWindowSize, useDynamicPosition, useOutsideClick } from './hooks'
2
- export { formatTime, formatDate, DATE_FORMATS } from './formatting'
2
+ export {
3
+ formatTime, formatDate, DATE_FORMATS, snakeCaseToTitleCase,
4
+ } from './formatting'
5
+ export { applyCharacterLimit } from './applyCharacterLimit'