@kitconcept/volto-light-theme 8.0.0-alpha.2 → 8.0.0-alpha.21

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 (101) hide show
  1. package/.changelog.draft +3 -4
  2. package/CHANGELOG.md +232 -0
  3. package/locales/de/LC_MESSAGES/volto.po +30 -115
  4. package/locales/en/LC_MESSAGES/volto.po +30 -115
  5. package/locales/es/LC_MESSAGES/volto.po +31 -116
  6. package/locales/eu/LC_MESSAGES/volto.po +58 -124
  7. package/locales/pt_BR/LC_MESSAGES/volto.po +38 -123
  8. package/locales/volto.pot +31 -116
  9. package/package.json +7 -4
  10. package/src/__mocks__/semantic-ui-react.ts +31 -0
  11. package/src/components/Blocks/Block/Edit.jsx +14 -6
  12. package/src/components/Blocks/Block/EditBlockWrapper.jsx +9 -3
  13. package/src/components/Blocks/Block/ErrorBoundary.test.tsx +55 -0
  14. package/src/components/Blocks/Block/ErrorBoundary.tsx +92 -0
  15. package/src/components/Blocks/Block/ErrorBoundaryMessage.tsx +66 -0
  16. package/src/components/Blocks/EventCalendar/Search/components/EventTemplate.tsx +1 -1
  17. package/src/components/Blocks/Image/Edit.jsx +1 -0
  18. package/src/components/Blocks/Listing/DefaultTemplate.jsx +12 -6
  19. package/src/components/Blocks/Listing/GridTemplate.jsx +16 -7
  20. package/src/components/Blocks/Listing/ListingBody.jsx +4 -1
  21. package/src/components/Blocks/Listing/SummaryTemplate.jsx +16 -7
  22. package/src/components/Blocks/Teaser/DefaultBody.tsx +25 -5
  23. package/src/components/Blocks/schema.ts +69 -0
  24. package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +128 -0
  25. package/src/components/Breadcrumbs/Breadcrumbs.tsx +117 -0
  26. package/src/components/Caption/Caption.test.tsx +31 -0
  27. package/src/components/Caption/{Caption.jsx → Caption.tsx} +14 -21
  28. package/src/components/Footer/ColumnLinks.tsx +2 -2
  29. package/src/components/Footer/slots/Colophon.tsx +13 -1
  30. package/src/components/Footer/slots/CoreFooter.tsx +4 -2
  31. package/src/components/Header/Header.tsx +3 -3
  32. package/src/components/LanguageSelector/LanguageSelector.tsx +91 -0
  33. package/src/components/MobileNavigation/MobileNavigation.jsx +11 -9
  34. package/src/components/Navigation/Navigation.test.tsx +176 -0
  35. package/src/components/Navigation/{Navigation.jsx → Navigation.tsx} +77 -37
  36. package/src/components/StickyMenu/MobileCarouselArrowButton.tsx +81 -0
  37. package/src/components/StickyMenu/MobileStickyMenu.tsx +76 -0
  38. package/src/components/Summary/DefaultSummary.tsx +10 -3
  39. package/src/components/Summary/EventSummary.tsx +10 -3
  40. package/src/components/Summary/FileSummary.tsx +10 -3
  41. package/src/components/Summary/NewsItemSummary.tsx +10 -3
  42. package/src/components/Summary/PersonSummary.tsx +10 -3
  43. package/src/components/Summary/Summary.stories.tsx +46 -30
  44. package/src/components/Tags/Tags.test.tsx +71 -0
  45. package/src/components/Tags/{Tags.jsx → Tags.tsx} +9 -25
  46. package/src/components/Theme/EventView.jsx +4 -4
  47. package/src/components/Theme/NewsItemView.jsx +4 -4
  48. package/src/components/Theme/RenderBlocks.jsx +45 -37
  49. package/src/components/Theme/RenderBlocksV2.jsx +51 -20
  50. package/src/components/Widgets/ColorSwatch.stories.tsx +197 -0
  51. package/src/components/Widgets/ColorSwatch.test.tsx +188 -0
  52. package/src/components/Widgets/ColorSwatch.tsx +77 -39
  53. package/src/components/Widgets/SoftTextWidget.tsx +129 -0
  54. package/src/components/Widgets/SoftTextareaWidget.tsx +118 -0
  55. package/src/components/Widgets/ThemeColorSwatch.tsx +5 -9
  56. package/src/config/blocks.tsx +21 -29
  57. package/src/config/slots.ts +7 -0
  58. package/src/config/widgets.ts +5 -9
  59. package/src/customizations/volto/components/manage/DragDropList/DragDropList.jsx +263 -0
  60. package/src/customizations/volto/components/theme/LanguageSelector/LanguageSelector.tsx +10 -0
  61. package/src/helpers/styleDefinitions.test.tsx +30 -0
  62. package/src/helpers/styleDefinitions.ts +49 -0
  63. package/src/internalChecks.test.ts +94 -0
  64. package/src/primitives/Card/Card.stories.tsx +4 -1
  65. package/src/primitives/Card/Card.test.tsx +11 -33
  66. package/src/primitives/Card/Card.tsx +33 -43
  67. package/src/primitives/IconLinkList.tsx +53 -52
  68. package/src/theme/_bgcolor-blocks-layout.scss +43 -45
  69. package/src/theme/_content.scss +12 -13
  70. package/src/theme/_export_import.scss +94 -0
  71. package/src/theme/_footer.scss +64 -19
  72. package/src/theme/_header.scss +21 -4
  73. package/src/theme/_insets.scss +1 -1
  74. package/src/theme/_layout.scss +34 -15
  75. package/src/theme/_mobile-sticky-menu.scss +92 -0
  76. package/src/theme/_search-page.scss +249 -0
  77. package/src/theme/_typo-custom.scss +16 -5
  78. package/src/theme/_variables.scss +19 -4
  79. package/src/theme/_widgets.scss +15 -27
  80. package/src/theme/blocks/_accordion.scss +11 -4
  81. package/src/theme/blocks/_grid.scss +9 -77
  82. package/src/theme/blocks/_listing.scss +60 -126
  83. package/src/theme/blocks/_search.scss +3 -4
  84. package/src/theme/blocks/_table.scss +1 -0
  85. package/src/theme/blocks/_teaser.scss +7 -117
  86. package/src/theme/blocks/error-boundary.scss +11 -0
  87. package/src/theme/card.scss +107 -70
  88. package/src/theme/main.scss +5 -0
  89. package/src/theme/notfound.scss +27 -0
  90. package/src/theme/person.scss +28 -12
  91. package/src/theme/sticky-menu.scss +7 -5
  92. package/src/types.d.ts +1 -0
  93. package/vitest.config.mjs +4 -0
  94. package/razzle.extend.js +0 -38
  95. package/src/components/Blocks/schema.js +0 -44
  96. package/src/components/Breadcrumbs/Breadcrumbs.jsx +0 -118
  97. package/src/components/Widgets/AlignWidget.tsx +0 -84
  98. package/src/components/Widgets/BlockAlignment.tsx +0 -88
  99. package/src/components/Widgets/BlockWidth.tsx +0 -101
  100. package/src/components/Widgets/Buttons.tsx +0 -144
  101. package/src/components/Widgets/Size.tsx +0 -78
@@ -0,0 +1,197 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import Wrapper from '@plone/volto/storybook';
4
+ import type { StyleDefinition } from '@plone/types';
5
+
6
+ import ColorSwatch, { type ColorSwatchProps } from './ColorSwatch';
7
+
8
+ const palettes: StyleDefinition[] = [
9
+ {
10
+ name: 'default',
11
+ label: 'Default',
12
+ style: {
13
+ '--theme-color': '#ffffff',
14
+ '--theme-foreground-color': '#1b1c1d',
15
+ '--theme-low-contrast-foreground-color': '#585858',
16
+ },
17
+ },
18
+ {
19
+ name: 'warm',
20
+ label: 'Warm',
21
+ style: {
22
+ '--theme-color': '#fff5ed',
23
+ '--theme-foreground-color': '#6b2c1f',
24
+ '--theme-low-contrast-foreground-color': '#b06a54',
25
+ },
26
+ },
27
+ {
28
+ name: 'unstyled',
29
+ label: 'No Inline Style',
30
+ style: undefined,
31
+ },
32
+ ];
33
+
34
+ const themePalettes: StyleDefinition[] = [
35
+ {
36
+ name: 'ocean',
37
+ label: 'Ocean',
38
+ style: {
39
+ '--theme-color': '#dff5ff',
40
+ '--theme-foreground-color': '#003d5b',
41
+ '--theme-low-contrast-foreground-color': '#5c6b73',
42
+ },
43
+ },
44
+ {
45
+ name: 'forest',
46
+ label: 'Forest',
47
+ style: {
48
+ '--theme-color': '#e6f4ea',
49
+ '--theme-foreground-color': '#1e4d2b',
50
+ '--theme-low-contrast-foreground-color': '#4b7754',
51
+ },
52
+ },
53
+ ];
54
+
55
+ const palettesWithoutDefault: StyleDefinition[] = [
56
+ {
57
+ name: 'primary',
58
+ label: 'Primary',
59
+ style: {
60
+ '--theme-color': '#333333',
61
+ '--theme-foreground-color': '#f2f2f2',
62
+ '--theme-low-contrast-foreground-color': '#d9d9d9',
63
+ },
64
+ },
65
+ {
66
+ name: 'secondary',
67
+ label: 'Secondary',
68
+ style: {
69
+ '--theme-color': '#666666',
70
+ '--theme-foreground-color': '#ffffff',
71
+ '--theme-low-contrast-foreground-color': '#f0f0f0',
72
+ },
73
+ },
74
+ ];
75
+
76
+ const meta = {
77
+ title: 'Widgets/ColorSwatch',
78
+ component: ColorSwatch,
79
+ parameters: {
80
+ layout: 'centered',
81
+ },
82
+ decorators: [
83
+ (Story) => (
84
+ <Wrapper>
85
+ <div style={{ width: 320 }}>
86
+ <Story />
87
+ </div>
88
+ </Wrapper>
89
+ ),
90
+ ],
91
+ tags: ['autodocs'],
92
+ } satisfies Meta<typeof ColorSwatch>;
93
+
94
+ export default meta;
95
+ type Story = StoryObj<typeof meta>;
96
+
97
+ const resolveSelectedColorName = (
98
+ args: ColorSwatchProps,
99
+ value: string | undefined,
100
+ ) => {
101
+ const colors = args.themes || args.colors || [];
102
+ const selectedColorName = colors.find(({ name }) => name === value)?.name;
103
+ const defaultSelectedColorName =
104
+ !selectedColorName && typeof args.default === 'string'
105
+ ? colors.find(({ name }) => name === args.default)?.name
106
+ : undefined;
107
+
108
+ if (selectedColorName || defaultSelectedColorName) {
109
+ return selectedColorName ?? defaultSelectedColorName;
110
+ }
111
+
112
+ return colors.find(({ name }) => name === 'default')?.name ?? colors[0]?.name;
113
+ };
114
+
115
+ const InteractiveColorSwatch = (args: ColorSwatchProps) => {
116
+ const [value, setValue] = React.useState(args.value);
117
+ const selectedValue = resolveSelectedColorName(args, value);
118
+
119
+ return (
120
+ <>
121
+ <ColorSwatch
122
+ {...args}
123
+ value={value}
124
+ onChange={(id, selectedValue) => {
125
+ setValue(selectedValue);
126
+ args.onChange?.(id, selectedValue);
127
+ }}
128
+ />
129
+ <div style={{ marginTop: '10px' }}>
130
+ The selected value is: <strong>{selectedValue || 'none'}</strong>
131
+ </div>
132
+ </>
133
+ );
134
+ };
135
+
136
+ export const NoPalettes: Story = {
137
+ render: (args) => <InteractiveColorSwatch {...args} />,
138
+ args: {
139
+ id: 'color',
140
+ label: 'Theme',
141
+ title: 'Theme',
142
+ themes: [],
143
+ },
144
+ };
145
+
146
+ export const DefaultPalette: Story = {
147
+ render: (args) => <InteractiveColorSwatch {...args} />,
148
+ args: {
149
+ id: 'color',
150
+ label: 'Color palette',
151
+ title: 'Color palette',
152
+ value: 'default',
153
+ colors: palettes,
154
+ },
155
+ };
156
+
157
+ export const ThemePalette: Story = {
158
+ render: (args) => <InteractiveColorSwatch {...args} />,
159
+ args: {
160
+ id: 'theme',
161
+ label: 'Theme palette',
162
+ title: 'Theme palette',
163
+ value: 'ocean',
164
+ themes: themePalettes,
165
+ },
166
+ };
167
+
168
+ export const ColorPaletteWithDefault: Story = {
169
+ render: (args) => <InteractiveColorSwatch {...args} />,
170
+ args: {
171
+ id: 'color',
172
+ label: 'Color palette',
173
+ title: 'Color palette',
174
+ colors: palettes,
175
+ default: 'warm',
176
+ },
177
+ };
178
+
179
+ export const ColorPaletteNoValueNoDefault: Story = {
180
+ render: (args) => <InteractiveColorSwatch {...args} />,
181
+ args: {
182
+ id: 'color',
183
+ label: 'Color palette',
184
+ title: 'Color palette',
185
+ colors: palettes,
186
+ },
187
+ };
188
+
189
+ export const ColorPaletteNoValueNoDefaultWithFallbackToFirst: Story = {
190
+ render: (args) => <InteractiveColorSwatch {...args} />,
191
+ args: {
192
+ id: 'color',
193
+ label: 'Color palette',
194
+ title: 'Color palette',
195
+ colors: palettesWithoutDefault,
196
+ },
197
+ };
@@ -0,0 +1,188 @@
1
+ import React, { createContext, useContext } from 'react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render, fireEvent } from '@testing-library/react';
4
+ import ColorSwatch from './ColorSwatch';
5
+ import type { StyleDefinition } from '@plone/types';
6
+
7
+ vi.mock('@plone/volto/components/manage/Widgets/FormFieldWrapper', () => ({
8
+ __esModule: true,
9
+ default: ({ children }: { children: React.ReactNode }) => (
10
+ <div data-testid="form-field-wrapper">{children}</div>
11
+ ),
12
+ }));
13
+
14
+ const MockRadioGroupContext = createContext<
15
+ ((value: string) => void) | undefined
16
+ >(undefined);
17
+
18
+ vi.mock('@plone/components', () => ({
19
+ RadioGroup: ({
20
+ children,
21
+ onChange,
22
+ ...rest
23
+ }: {
24
+ children: React.ReactNode;
25
+ onChange?: (value: string) => void;
26
+ }) => {
27
+ const { isDisabled: _isDisabled, ...passThrough } = rest as Record<
28
+ string,
29
+ unknown
30
+ >;
31
+
32
+ return (
33
+ <MockRadioGroupContext.Provider value={onChange}>
34
+ <div role="radiogroup" {...passThrough}>
35
+ {React.Children.map(children, (child) =>
36
+ React.isValidElement(child)
37
+ ? React.cloneElement(child, {
38
+ onSelect: onChange,
39
+ })
40
+ : child,
41
+ )}
42
+ </div>
43
+ </MockRadioGroupContext.Provider>
44
+ );
45
+ },
46
+ Radio: ({
47
+ value,
48
+ onSelect,
49
+ className,
50
+ children,
51
+ ...rest
52
+ }: {
53
+ value: string;
54
+ onSelect?: (value: string) => void;
55
+ className?: string;
56
+ children?: React.ReactNode;
57
+ }) => {
58
+ const groupOnChange = useContext(MockRadioGroupContext);
59
+ return (
60
+ <button
61
+ type="button"
62
+ className={className}
63
+ data-value={value}
64
+ onClick={() => {
65
+ groupOnChange?.(value);
66
+ onSelect?.(value);
67
+ }}
68
+ {...rest}
69
+ >
70
+ {children}
71
+ </button>
72
+ );
73
+ },
74
+ Tooltip: ({ children }: { children: React.ReactNode }) => (
75
+ <span data-testid="tooltip">{children}</span>
76
+ ),
77
+ }));
78
+
79
+ vi.mock('react-aria-components', () => ({
80
+ Focusable: ({ children }: { children: React.ReactNode }) => <>{children}</>,
81
+ TooltipTrigger: ({ children }: { children: React.ReactNode }) => (
82
+ <>{children}</>
83
+ ),
84
+ }));
85
+
86
+ const palette: StyleDefinition[] = [
87
+ {
88
+ name: 'default',
89
+ label: 'Default',
90
+ style: { '--theme-color': '#fff' },
91
+ },
92
+ {
93
+ name: 'ocean',
94
+ label: 'Ocean',
95
+ style: { '--theme-color': '#00a0e6' },
96
+ },
97
+ {
98
+ name: 'sunset',
99
+ label: 'Sunset',
100
+ style: { '--theme-color': '#ff6b00' },
101
+ },
102
+ ];
103
+
104
+ const renderColorSwatch = (
105
+ props: Partial<React.ComponentProps<typeof ColorSwatch>> = {},
106
+ ) => {
107
+ const { colors = palette, themes, onChange = vi.fn(), ...rest } = props;
108
+
109
+ const componentProps: React.ComponentProps<typeof ColorSwatch> = {
110
+ id: 'theme',
111
+ label: 'Theme',
112
+ title: 'Theme',
113
+ colors,
114
+ onChange,
115
+ ...rest,
116
+ } as React.ComponentProps<typeof ColorSwatch>;
117
+
118
+ if (themes !== undefined) {
119
+ componentProps.themes = themes;
120
+ }
121
+
122
+ return render(<ColorSwatch {...componentProps} />);
123
+ };
124
+
125
+ const getActiveClassName = (container: HTMLElement | Document) =>
126
+ container.querySelector('.color-swatch-option-handler.active')?.className ??
127
+ '';
128
+
129
+ describe('ColorSwatch', () => {
130
+ it('renders nothing when no palettes are provided', () => {
131
+ const { container } = renderColorSwatch({ themes: [] });
132
+ expect(container.firstChild).toBeNull();
133
+ });
134
+
135
+ it('highlights the provided value', () => {
136
+ const { container } = renderColorSwatch({ value: 'ocean' });
137
+ expect(getActiveClassName(container)).toContain('ocean');
138
+ });
139
+
140
+ it('highlights the default value when no current value exists', () => {
141
+ const { container } = renderColorSwatch({
142
+ value: undefined,
143
+ default: 'sunset',
144
+ });
145
+ expect(getActiveClassName(container)).toContain('sunset');
146
+ });
147
+
148
+ it('falls back to the "default" palette name when no value or default is provided', () => {
149
+ const { container } = renderColorSwatch({
150
+ value: undefined,
151
+ default: undefined,
152
+ });
153
+ expect(getActiveClassName(container)).toContain('default');
154
+ });
155
+
156
+ it('falls back to the first color when no "default" palette is present', () => {
157
+ const colorsWithoutDefault: StyleDefinition[] = [
158
+ {
159
+ name: 'primary',
160
+ label: 'Primary',
161
+ style: { '--theme-color': '#333' },
162
+ },
163
+ {
164
+ name: 'secondary',
165
+ label: 'Secondary',
166
+ style: { '--theme-color': '#666' },
167
+ },
168
+ ];
169
+
170
+ const { container } = renderColorSwatch({
171
+ colors: colorsWithoutDefault,
172
+ value: undefined,
173
+ default: undefined,
174
+ });
175
+
176
+ expect(getActiveClassName(container)).toContain('primary');
177
+ });
178
+
179
+ it('notifies when a different swatch is chosen', () => {
180
+ const handleChange = vi.fn();
181
+ const { container } = renderColorSwatch({ onChange: handleChange });
182
+ const buttons = container.querySelectorAll('.color-swatch-option-wrapper');
183
+
184
+ fireEvent.click(buttons[2]);
185
+
186
+ expect(handleChange).toHaveBeenCalledWith('theme', 'sunset');
187
+ });
188
+ });
@@ -1,58 +1,96 @@
1
1
  import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
2
- import { Button } from '@plone/components';
2
+ import { Radio, RadioGroup, Tooltip } from '@plone/components';
3
3
  import cx from 'classnames';
4
+ import { Focusable, TooltipTrigger } from 'react-aria-components';
5
+ import type { StyleDefinition } from '@plone/types';
4
6
 
5
- export type Color =
6
- | {
7
- name: string;
8
- label: string;
9
- style: Record<`--${string}`, string>;
10
- }
11
- | {
12
- name: string;
13
- label: string;
14
- style: undefined;
15
- };
16
-
17
- export type ColorSwatchProps = {
7
+ type BaseColorSwatchProps = {
18
8
  id: string;
19
9
  title: string;
10
+ label: string;
20
11
  value?: string;
21
- default?: string | object;
12
+ default?: string;
22
13
  required?: boolean;
23
- missing_value?: unknown;
24
14
  className?: string;
25
15
  onChange: (id: string, value: any) => void;
26
- colors: Color[];
27
- themes: Color[];
16
+ disabled?: boolean;
17
+ isDisabled?: boolean;
28
18
  };
29
19
 
20
+ type ColorsOnly = { colors: StyleDefinition[]; themes?: undefined };
21
+ type ThemesOnly = { themes: StyleDefinition[]; colors?: undefined };
22
+
23
+ export type ColorSwatchProps = BaseColorSwatchProps & (ColorsOnly | ThemesOnly);
24
+
30
25
  const ColorSwatch = (props: ColorSwatchProps) => {
31
- const { id, value, onChange } = props;
26
+ const {
27
+ id,
28
+ label,
29
+ title,
30
+ value,
31
+ onChange,
32
+ disabled,
33
+ isDisabled,
34
+ default: defaultValue,
35
+ } = props;
32
36
  const colors = props.themes || props.colors || [];
33
37
 
38
+ const selectedColorName = colors.find(({ name }) => name === value)?.name;
39
+ const defaultSelectedColorName =
40
+ !selectedColorName && typeof defaultValue === 'string'
41
+ ? colors.find(({ name }) => name === defaultValue)?.name
42
+ : undefined;
43
+
44
+ const fallbackColorName =
45
+ !selectedColorName && !defaultSelectedColorName
46
+ ? colors.find(({ name }) => name === 'default')?.name || colors[0]?.name
47
+ : undefined;
48
+
49
+ const radioGroupValueProps: {
50
+ value?: string;
51
+ defaultValue?: string;
52
+ } = selectedColorName
53
+ ? { value: selectedColorName }
54
+ : defaultSelectedColorName
55
+ ? { defaultValue: defaultSelectedColorName }
56
+ : fallbackColorName
57
+ ? { defaultValue: fallbackColorName }
58
+ : {};
59
+
60
+ const currentColorName =
61
+ selectedColorName ?? defaultSelectedColorName ?? fallbackColorName;
62
+
34
63
  return colors.length > 0 ? (
35
64
  <FormFieldWrapper {...props} className="color-swatch-widget">
36
- <div className="buttons">
37
- {colors.map((color) => {
38
- const colorName = color.name;
39
- return (
40
- <Button
41
- key={id + colorName}
42
- className={cx(colorName, { active: value === colorName })}
43
- onPress={(e) => {
44
- onChange(
45
- id,
46
- colorName === value ? props.missing_value : colorName,
47
- );
48
- }}
49
- value={value}
50
- style={color.style}
51
- aria-label={color.label}
52
- />
53
- );
54
- })}
55
- </div>
65
+ <RadioGroup
66
+ aria-label={title || label || id}
67
+ orientation="horizontal"
68
+ {...radioGroupValueProps}
69
+ onChange={(value) => onChange(id, value)}
70
+ isDisabled={disabled || isDisabled}
71
+ >
72
+ {colors.map((color) => (
73
+ <Radio
74
+ aria-label={color.label}
75
+ value={color.name}
76
+ className="color-swatch-option-wrapper"
77
+ key={color.name}
78
+ >
79
+ <TooltipTrigger delay={120} closeDelay={80} trigger="hover">
80
+ <Focusable>
81
+ <div
82
+ role="img"
83
+ className={cx('color-swatch-option-handler', color.name, {
84
+ active: currentColorName === color.name,
85
+ })}
86
+ style={color.style}
87
+ ></div>
88
+ </Focusable>
89
+ <Tooltip>{color.label}</Tooltip>
90
+ </TooltipTrigger>
91
+ </Radio>
92
+ ))}
93
+ </RadioGroup>
56
94
  </FormFieldWrapper>
57
95
  ) : null;
58
96
  };
@@ -0,0 +1,129 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Input, Label } from 'semantic-ui-react';
4
+ import Icon from '@plone/volto/components/theme/Icon/Icon';
5
+ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
6
+
7
+ const SoftTextWidget = (props) => {
8
+ const {
9
+ id,
10
+ value,
11
+ onChange,
12
+ onBlur,
13
+ onClick,
14
+ icon,
15
+ iconAction,
16
+ minLength,
17
+ maxLength,
18
+ softMaxLength,
19
+ placeholder,
20
+ isDisabled,
21
+ focus,
22
+ } = props;
23
+ const ref = useRef();
24
+ const [softLengthWarning, setSoftLengthWarning] = useState('');
25
+ useEffect(() => {
26
+ if (focus) {
27
+ ref.current.focus();
28
+ }
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, []);
31
+ // START CUSTOMIZATION
32
+ const handleChange = (id, newValue) => {
33
+ if (softMaxLength && newValue?.length) {
34
+ const remaining = softMaxLength - newValue.length;
35
+ if (remaining < 0) {
36
+ setSoftLengthWarning(
37
+ `You have exceeded the recommended limit by ${Math.abs(remaining)}`,
38
+ );
39
+ } else {
40
+ setSoftLengthWarning('');
41
+ }
42
+ }
43
+ onChange(id, newValue);
44
+ };
45
+ // END CUSTOMIZATION
46
+ return (
47
+ <FormFieldWrapper {...props} className="text">
48
+ <Input
49
+ id={`field-${id}`}
50
+ name={id}
51
+ value={value || ''}
52
+ disabled={isDisabled}
53
+ icon={icon || null}
54
+ placeholder={placeholder}
55
+ // START CUSTOMIZATION
56
+ onChange={({ target }) =>
57
+ handleChange(id, target.value === '' ? undefined : target.value)
58
+ }
59
+ // END CUSTOMIZATION
60
+ ref={ref}
61
+ onBlur={({ target }) =>
62
+ onBlur(id, target.value === '' ? undefined : target.value)
63
+ }
64
+ onClick={() => onClick()}
65
+ minLength={minLength || null}
66
+ maxLength={maxLength || null}
67
+ />
68
+ {/* START CUSTOMIZATION */}
69
+ {softLengthWarning.length > 0 && (
70
+ <Label key={softLengthWarning} basic color="yellow" pointing>
71
+ {softLengthWarning}
72
+ </Label>
73
+ )}
74
+ {/* END CUSTOMIZATION */}
75
+ {icon && iconAction && (
76
+ <button className={`field-${id}-action-button`} onClick={iconAction}>
77
+ <Icon name={icon} size="18px" />
78
+ </button>
79
+ )}
80
+ </FormFieldWrapper>
81
+ );
82
+ };
83
+ export default SoftTextWidget;
84
+ SoftTextWidget.propTypes = {
85
+ id: PropTypes.string.isRequired,
86
+ title: PropTypes.string.isRequired,
87
+ description: PropTypes.string,
88
+ required: PropTypes.bool,
89
+ error: PropTypes.arrayOf(PropTypes.string),
90
+ value: PropTypes.string,
91
+ focus: PropTypes.bool,
92
+ onChange: PropTypes.func,
93
+ onBlur: PropTypes.func,
94
+ onClick: PropTypes.func,
95
+ onEdit: PropTypes.func,
96
+ onDelete: PropTypes.func,
97
+ icon: PropTypes.shape({
98
+ xmlns: PropTypes.string,
99
+ viewBox: PropTypes.string,
100
+ content: PropTypes.string,
101
+ }),
102
+ iconAction: PropTypes.func,
103
+ minLength: PropTypes.number,
104
+ maxLength: PropTypes.number,
105
+ // START CUSTOMIZATION
106
+ softMaxLength: PropTypes.number,
107
+ // END CUSTOMIZATION
108
+ wrapped: PropTypes.bool,
109
+ placeholder: PropTypes.string,
110
+ };
111
+ SoftTextWidget.defaultProps = {
112
+ description: null,
113
+ required: false,
114
+ error: [],
115
+ value: null,
116
+ onChange: () => {},
117
+ onBlur: () => {},
118
+ onClick: () => {},
119
+ onEdit: null,
120
+ onDelete: null,
121
+ focus: false,
122
+ icon: null,
123
+ iconAction: null,
124
+ minLength: null,
125
+ maxLength: null,
126
+ // START CUSTOMIZATION
127
+ softMaxLength: null,
128
+ // END CUSTOMIZATION
129
+ };