@kitconcept/volto-light-theme 6.0.0-alpha.8 → 6.0.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.
- package/.changelog.draft +1 -5
- package/CHANGELOG.md +226 -0
- package/README.md +6 -5
- package/locales/de/LC_MESSAGES/volto.po +171 -38
- package/locales/en/LC_MESSAGES/volto.po +170 -37
- package/locales/es/LC_MESSAGES/volto.po +171 -38
- package/locales/eu/LC_MESSAGES/volto.po +171 -38
- package/locales/pt_BR/volto.po +171 -38
- package/locales/volto.pot +171 -38
- package/package.json +15 -6
- package/src/components/Blocks/EventMetadata/View.jsx +32 -26
- package/src/components/Blocks/Listing/DefaultTemplate.jsx +19 -14
- package/src/components/Blocks/Listing/GridTemplate.jsx +9 -12
- package/src/components/Blocks/Listing/SummaryTemplate.jsx +9 -7
- package/src/components/Blocks/Teaser/DefaultBody.jsx +93 -0
- package/src/components/Blocks/Teaser/schema.js +1 -7
- package/src/components/Footer/ColumnLinks.tsx +35 -0
- package/src/components/Footer/Footer.tsx +32 -0
- package/src/components/Footer/slots/Colophon.tsx +24 -0
- package/src/components/Footer/slots/Copyright.tsx +65 -0
- package/src/components/Footer/slots/CoreFooter.tsx +82 -0
- package/src/components/Footer/slots/FollowUsLogoAndLinks.tsx +80 -0
- package/src/components/Footer/slots/FooterLogos.tsx +44 -0
- package/src/components/Header/Header.tsx +257 -0
- package/src/components/Logo/Logo.tsx +85 -0
- package/src/components/{Footer/FooterLogos.tsx → LogosContainer/LogosContainer.tsx} +24 -25
- package/src/components/MobileNavigation/MobileNavigation.jsx +53 -18
- package/src/components/Navigation/Navigation.jsx +14 -3
- package/src/components/SearchWidget/IntranetSearchWidget.jsx +32 -5
- package/src/components/SearchWidget/SearchWidget.jsx +1 -1
- package/src/components/StickyMenu/StickyMenu.tsx +36 -0
- package/src/components/Summary/DefaultSummary.jsx +16 -0
- package/src/components/Summary/EventSummary.jsx +38 -0
- package/src/components/Summary/FileSummary.jsx +24 -0
- package/src/components/Summary/NewsItemSummary.jsx +40 -0
- package/src/components/Tags/Tags.jsx +46 -0
- package/src/components/Theme/EventView.jsx +19 -25
- package/src/components/Theme/NewsItemView.jsx +13 -9
- package/src/components/Theming/Theming.tsx +20 -17
- package/src/components/Widgets/{BlockAlignmentWidget.tsx → BlockAlignment.tsx} +9 -2
- package/src/components/Widgets/{BlockWidthWidget.tsx → BlockWidth.tsx} +10 -3
- package/src/components/Widgets/BlocksObject.tsx +353 -0
- package/src/components/Widgets/Buttons.tsx +117 -0
- package/src/components/Widgets/ColorContrastChecker.tsx +117 -0
- package/src/components/Widgets/ColorPicker.tsx +59 -0
- package/src/components/Widgets/{ColorPickerWidget.tsx → ColorSwatch.tsx} +5 -5
- package/src/components/Widgets/ObjectList.tsx +342 -0
- package/src/components/Widgets/{ThemingColorPicker.tsx → RACThemingColorPicker.tsx} +4 -0
- package/src/components/Widgets/Size.tsx +75 -0
- package/src/components/Widgets/ThemeColorSwatch.tsx +17 -0
- package/src/components/Widgets/schema/footerLinksSchema.ts +64 -0
- package/src/components/Widgets/schema/footerLogosSchema.ts +98 -0
- package/src/components/Widgets/schema/headerActionsSchema.ts +64 -0
- package/src/components/Widgets/schema/iconLinkListSchema.ts +98 -0
- package/src/config/blocks.tsx +39 -20
- package/src/config/settings.ts +54 -12
- package/src/config/slots.ts +36 -1
- package/src/config/summary.ts +24 -0
- package/src/config/widgets.ts +57 -20
- package/src/customizations/volto/components/manage/Blocks/Teaser/DefaultBody.jsx +8 -0
- package/src/customizations/volto/components/theme/Tags/Tags.jsx +11 -0
- package/src/customizations/volto/components/theme/View/RenderBlocks.jsx +2 -1
- package/src/helpers/DndSortableList.tsx +138 -0
- package/src/helpers/dates.js +22 -0
- package/src/helpers/doesNodeContainClick.js +64 -0
- package/src/helpers/useLiveData.ts +29 -0
- package/src/index.ts +33 -2
- package/src/primitives/IconLinkList.tsx +69 -0
- package/src/primitives/LinkList.tsx +35 -0
- package/src/theme/_bgcolor-blocks-layout.scss +50 -12
- package/src/theme/_container.scss +4 -0
- package/src/theme/_content.scss +6 -0
- package/src/theme/_footer.scss +295 -43
- package/src/theme/_header.scss +132 -19
- package/src/theme/_layout.scss +11 -1
- package/src/theme/_sitemap.scss +4 -0
- package/src/theme/_utils.scss +14 -1
- package/src/theme/_variables.scss +12 -3
- package/src/theme/_widgets.scss +102 -10
- package/src/theme/blocks/_eventMetadata.scss +5 -2
- package/src/theme/blocks/_grid.scss +3 -3
- package/src/theme/blocks/_highlight.scss +17 -44
- package/src/theme/blocks/_listing.scss +25 -16
- package/src/theme/blocks/_maps.scss +3 -3
- package/src/theme/blocks/_slider.scss +5 -1
- package/src/theme/main.scss +1 -0
- package/src/theme/sticky-menu.scss +50 -0
- package/src/types.d.ts +102 -0
- package/tsconfig.json +1 -1
- package/src/components/Footer/Footer.jsx +0 -115
- package/src/components/Footer/FooterLinks.tsx +0 -57
- package/src/components/Header/Header.jsx +0 -161
- package/src/components/Logo/Logo.jsx +0 -51
- package/src/components/Widgets/AlignWidget.jsx +0 -80
- package/src/components/Widgets/BackgroundColorWidget.tsx +0 -17
- package/src/components/Widgets/BlocksObjectWidget.tsx +0 -333
- package/src/components/Widgets/ButtonsWidget.tsx +0 -68
- package/src/components/Widgets/FooterLinksWidget.tsx +0 -106
- package/src/components/Widgets/FooterLogosWidget.tsx +0 -120
- package/src/static/container-query-polyfill.modern.js +0 -1
- package/src/types/index.d.ts +0 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import omit from 'lodash/omit';
|
|
4
|
+
import { Button } from '@plone/components';
|
|
5
|
+
import { Text } from 'react-aria-components';
|
|
6
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
7
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
8
|
+
import { applySchemaDefaults } from '@plone/volto/helpers/Blocks/Blocks';
|
|
9
|
+
import AnimateHeight from 'react-animate-height';
|
|
10
|
+
import ObjectWidget from '@plone/volto/components/manage/Widgets/ObjectWidget';
|
|
11
|
+
import {
|
|
12
|
+
getBlocksFieldname,
|
|
13
|
+
getBlocksLayoutFieldname,
|
|
14
|
+
moveBlock,
|
|
15
|
+
} from '@plone/volto/helpers/Blocks/Blocks';
|
|
16
|
+
import DndSortableList from '../../helpers/DndSortableList';
|
|
17
|
+
import cx from 'classnames';
|
|
18
|
+
import upSVG from '@plone/volto/icons/up-key.svg';
|
|
19
|
+
import downSVG from '@plone/volto/icons/down-key.svg';
|
|
20
|
+
import deleteSVG from '@plone/volto/icons/delete.svg';
|
|
21
|
+
import addSVG from '@plone/volto/icons/add.svg';
|
|
22
|
+
import dragSVG from '@plone/volto/icons/drag.svg';
|
|
23
|
+
import { v4 as uuid } from 'uuid';
|
|
24
|
+
import type {
|
|
25
|
+
BlockConfigBase,
|
|
26
|
+
BlocksData,
|
|
27
|
+
Content,
|
|
28
|
+
JSONSchema,
|
|
29
|
+
} from '@plone/types';
|
|
30
|
+
import type { IntlShape } from 'react-intl';
|
|
31
|
+
import config from '@plone/volto/registry';
|
|
32
|
+
|
|
33
|
+
const messages = defineMessages({
|
|
34
|
+
labelRemoveItem: {
|
|
35
|
+
id: 'Remove item',
|
|
36
|
+
defaultMessage: 'Remove item',
|
|
37
|
+
},
|
|
38
|
+
labelCollapseItem: {
|
|
39
|
+
id: 'Collapse item',
|
|
40
|
+
defaultMessage: 'Collapse item',
|
|
41
|
+
},
|
|
42
|
+
labelShowItem: {
|
|
43
|
+
id: 'Show item',
|
|
44
|
+
defaultMessage: 'Show item',
|
|
45
|
+
},
|
|
46
|
+
emptyObjectList: {
|
|
47
|
+
id: 'Empty object list',
|
|
48
|
+
defaultMessage: 'Empty object list',
|
|
49
|
+
},
|
|
50
|
+
add: {
|
|
51
|
+
id: 'Add (object list)',
|
|
52
|
+
defaultMessage: 'Add',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export type BlocksObjectWidgetSchema =
|
|
57
|
+
| (JSONSchema & { addMessage: string })
|
|
58
|
+
| ((props: BlocksObjectWidgetProps) => JSONSchema & { addMessage: string });
|
|
59
|
+
|
|
60
|
+
export type BlocksObjectWidgetProps = {
|
|
61
|
+
/**
|
|
62
|
+
* The ID of the widget.
|
|
63
|
+
*/
|
|
64
|
+
id: string;
|
|
65
|
+
/**
|
|
66
|
+
* The ID of the block this widget belongs to.
|
|
67
|
+
*/
|
|
68
|
+
block: string;
|
|
69
|
+
/**
|
|
70
|
+
* The fieldset this widget belongs to.
|
|
71
|
+
*/
|
|
72
|
+
fieldSet: string;
|
|
73
|
+
/**
|
|
74
|
+
* The title of the widget.
|
|
75
|
+
*/
|
|
76
|
+
title: string;
|
|
77
|
+
/**
|
|
78
|
+
* The current value of the widget, which is BlocksData.
|
|
79
|
+
*/
|
|
80
|
+
value?: BlocksData;
|
|
81
|
+
/**
|
|
82
|
+
* The default value for the widget. Can be a string or an object.
|
|
83
|
+
*/
|
|
84
|
+
default?: string | object;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the widget is required.
|
|
87
|
+
*/
|
|
88
|
+
required?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* The value to use when the widget is missing a value.
|
|
91
|
+
*/
|
|
92
|
+
missing_value?: unknown;
|
|
93
|
+
/**
|
|
94
|
+
* The CSS class name for the widget.
|
|
95
|
+
*/
|
|
96
|
+
className?: string;
|
|
97
|
+
/**
|
|
98
|
+
* A callback function that is called when the value of the widget changes.
|
|
99
|
+
* @param id The ID of the widget.
|
|
100
|
+
* @param value The new value of the widget.
|
|
101
|
+
*/
|
|
102
|
+
onChange: (id: string, value: any) => void;
|
|
103
|
+
/**
|
|
104
|
+
* The index of the currently active object.
|
|
105
|
+
*/
|
|
106
|
+
activeObject: number;
|
|
107
|
+
/**
|
|
108
|
+
* A callback function that is called to set the active object.
|
|
109
|
+
* @param index The index of the object to set as active.
|
|
110
|
+
*/
|
|
111
|
+
setActiveObject: (index: number) => void;
|
|
112
|
+
/**
|
|
113
|
+
* The schema for the BlocksObjectWidget.
|
|
114
|
+
*/
|
|
115
|
+
schema: BlocksObjectWidgetSchema;
|
|
116
|
+
/**
|
|
117
|
+
* The name of the schema.
|
|
118
|
+
*/
|
|
119
|
+
schemaName: string;
|
|
120
|
+
/**
|
|
121
|
+
* An optional function to enhance the schema.
|
|
122
|
+
* @param args An object containing the schema, form data, intl, navRoot, and contentType.
|
|
123
|
+
*/
|
|
124
|
+
schemaEnhancer?: (args: {
|
|
125
|
+
schema: JSONSchema & { addMessage: string };
|
|
126
|
+
formData: BlockConfigBase;
|
|
127
|
+
intl: IntlShape;
|
|
128
|
+
navRoot: Content;
|
|
129
|
+
contentType: string;
|
|
130
|
+
}) => JSONSchema;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function deleteBlock(formData, blockId: string) {
|
|
134
|
+
const blocksFieldname = getBlocksFieldname(formData);
|
|
135
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
|
|
136
|
+
|
|
137
|
+
let newFormData = {
|
|
138
|
+
...formData,
|
|
139
|
+
[blocksLayoutFieldname]: {
|
|
140
|
+
items: formData[blocksLayoutFieldname].items.filter(
|
|
141
|
+
(value) => value !== blockId,
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
[blocksFieldname]: omit(formData[blocksFieldname], [blockId]),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return newFormData;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const BlocksObjectWidget = (props: BlocksObjectWidgetProps) => {
|
|
151
|
+
const { block, fieldSet, id, value, onChange, schemaEnhancer, schemaName } =
|
|
152
|
+
props;
|
|
153
|
+
|
|
154
|
+
const schema = config.getUtility({
|
|
155
|
+
type: 'schema',
|
|
156
|
+
name: schemaName,
|
|
157
|
+
}).method;
|
|
158
|
+
|
|
159
|
+
const [localActiveObject, setLocalActiveObject] = useState(
|
|
160
|
+
props.activeObject ?? value?.blocks_layout?.items?.length - 1,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let activeObject: number, setActiveObject: (index: number) => void;
|
|
164
|
+
if (
|
|
165
|
+
(props.activeObject || props.activeObject === 0) &&
|
|
166
|
+
props.setActiveObject
|
|
167
|
+
) {
|
|
168
|
+
activeObject = props.activeObject;
|
|
169
|
+
setActiveObject = props.setActiveObject;
|
|
170
|
+
} else {
|
|
171
|
+
activeObject = localActiveObject;
|
|
172
|
+
setActiveObject = setLocalActiveObject;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const intl = useIntl();
|
|
176
|
+
|
|
177
|
+
function handleChangeActiveObject(index) {
|
|
178
|
+
const newIndex = activeObject === index ? -1 : index;
|
|
179
|
+
|
|
180
|
+
setActiveObject(newIndex);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const objectSchema = typeof schema === 'function' ? schema(props) : schema;
|
|
184
|
+
|
|
185
|
+
function handleDragEnd(event) {
|
|
186
|
+
const { active, over } = event;
|
|
187
|
+
|
|
188
|
+
if (active.id !== over.id) {
|
|
189
|
+
const source = value.blocks_layout.items.indexOf(active.id);
|
|
190
|
+
const destination = value.blocks_layout.items.indexOf(over.id);
|
|
191
|
+
|
|
192
|
+
const newFormData = moveBlock(value, source, destination);
|
|
193
|
+
onChange(id, newFormData);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="blocks-object-widget">
|
|
199
|
+
<FormFieldWrapper {...props} noForInFieldLabel className="objectlist">
|
|
200
|
+
<div className="add-item-button-wrapper">
|
|
201
|
+
<Button
|
|
202
|
+
aria-label={
|
|
203
|
+
objectSchema.addMessage ||
|
|
204
|
+
`${intl.formatMessage(messages.add)} ${objectSchema.title}`
|
|
205
|
+
}
|
|
206
|
+
onPress={(e) => {
|
|
207
|
+
const newId = uuid();
|
|
208
|
+
const data = {};
|
|
209
|
+
|
|
210
|
+
const objSchema = schemaEnhancer
|
|
211
|
+
? // @ts-ignore - TODO Make sure this continues to have sense
|
|
212
|
+
schemaEnhancer({ schema: objectSchema, formData: data, intl })
|
|
213
|
+
: objectSchema;
|
|
214
|
+
const dataWithDefaults = applySchemaDefaults({
|
|
215
|
+
data,
|
|
216
|
+
schema: objSchema,
|
|
217
|
+
intl,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
onChange(id, {
|
|
221
|
+
...(value || {}),
|
|
222
|
+
blocks: {
|
|
223
|
+
...value?.blocks,
|
|
224
|
+
[newId]: dataWithDefaults,
|
|
225
|
+
},
|
|
226
|
+
blocks_layout: {
|
|
227
|
+
...value?.blocks_layout,
|
|
228
|
+
items: [...(value?.blocks_layout?.items || []), newId],
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
setActiveObject(value?.blocks_layout?.items?.length || 0);
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
<Icon name={addSVG} size="18px" />
|
|
236
|
+
{/* Custom addMessage in schema, else default to English */}
|
|
237
|
+
<Text>
|
|
238
|
+
{objectSchema.addMessage ||
|
|
239
|
+
`${intl.formatMessage(messages.add)} ${objectSchema.title}`}
|
|
240
|
+
</Text>
|
|
241
|
+
</Button>
|
|
242
|
+
</div>
|
|
243
|
+
{value?.blocks_layout?.items?.length === 0 && (
|
|
244
|
+
<input
|
|
245
|
+
aria-labelledby={`fieldset-${
|
|
246
|
+
fieldSet || 'default'
|
|
247
|
+
}-field-label-${id}`}
|
|
248
|
+
type="hidden"
|
|
249
|
+
value={intl.formatMessage(messages.emptyObjectList)}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
</FormFieldWrapper>
|
|
253
|
+
<DndSortableList
|
|
254
|
+
// TODO: adapt it to the new DndSortableList shape
|
|
255
|
+
sortedItems={value?.blocks_layout?.items || []}
|
|
256
|
+
items={value?.blocks}
|
|
257
|
+
handleDragEnd={handleDragEnd}
|
|
258
|
+
activeObject={activeObject}
|
|
259
|
+
setActiveObject={setActiveObject}
|
|
260
|
+
>
|
|
261
|
+
{({ item, uid, index, attributes, listeners }) => {
|
|
262
|
+
return (
|
|
263
|
+
<div
|
|
264
|
+
className={cx('bow-item-wrapper', {
|
|
265
|
+
active: activeObject === index,
|
|
266
|
+
})}
|
|
267
|
+
key={uid}
|
|
268
|
+
>
|
|
269
|
+
<div
|
|
270
|
+
className="bow-item-title-bar"
|
|
271
|
+
onClick={() => handleChangeActiveObject(index)}
|
|
272
|
+
role="presentation"
|
|
273
|
+
aria-label={`${
|
|
274
|
+
activeObject === index
|
|
275
|
+
? intl.formatMessage(messages.labelCollapseItem)
|
|
276
|
+
: intl.formatMessage(messages.labelShowItem)
|
|
277
|
+
} #${index ? index + 1 : ''}`}
|
|
278
|
+
>
|
|
279
|
+
<div className="drag handle" {...listeners} {...attributes}>
|
|
280
|
+
<Icon name={dragSVG} size="18px" />
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="bow-item-title">
|
|
284
|
+
{item.title ||
|
|
285
|
+
`${objectSchema.title} #${index !== undefined ? index + 1 : ''}`}
|
|
286
|
+
</div>
|
|
287
|
+
<div className="bow-tools">
|
|
288
|
+
<button
|
|
289
|
+
aria-label={`${intl.formatMessage(
|
|
290
|
+
messages.labelRemoveItem,
|
|
291
|
+
)} #${index + 1}`}
|
|
292
|
+
onClick={() => {
|
|
293
|
+
onChange(id, deleteBlock(value, uid));
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<Icon name={deleteSVG} size="20px" color="#e40166" />
|
|
297
|
+
</button>
|
|
298
|
+
{activeObject === index ? (
|
|
299
|
+
<Icon name={upSVG} size="20px" />
|
|
300
|
+
) : (
|
|
301
|
+
<Icon name={downSVG} size="20px" />
|
|
302
|
+
)}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
<AnimateHeight
|
|
306
|
+
animateOpacity
|
|
307
|
+
duration={300}
|
|
308
|
+
height={activeObject === index ? 'auto' : 0}
|
|
309
|
+
>
|
|
310
|
+
<div
|
|
311
|
+
className={cx('bow-item-content', {
|
|
312
|
+
active: activeObject === index,
|
|
313
|
+
})}
|
|
314
|
+
>
|
|
315
|
+
<ObjectWidget
|
|
316
|
+
id={`${uid}`}
|
|
317
|
+
key={`bow-${uid}`}
|
|
318
|
+
block={block}
|
|
319
|
+
schema={
|
|
320
|
+
schemaEnhancer
|
|
321
|
+
? // @ts-ignore - TODO Make sure this continues to have sense
|
|
322
|
+
schemaEnhancer({
|
|
323
|
+
schema: objectSchema,
|
|
324
|
+
formData: item,
|
|
325
|
+
intl,
|
|
326
|
+
})
|
|
327
|
+
: objectSchema
|
|
328
|
+
}
|
|
329
|
+
value={item}
|
|
330
|
+
onChange={(fieldId: string, fieldValue: any) => {
|
|
331
|
+
const newvalue = {
|
|
332
|
+
...value.blocks[fieldId],
|
|
333
|
+
...fieldValue,
|
|
334
|
+
};
|
|
335
|
+
onChange(id, {
|
|
336
|
+
...value,
|
|
337
|
+
blocks: {
|
|
338
|
+
...value.blocks,
|
|
339
|
+
[fieldId]: newvalue,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
</AnimateHeight>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}}
|
|
349
|
+
</DndSortableList>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
export default BlocksObjectWidget;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
3
|
+
import Icon from '@plone/volto/components/theme/Icon/Icon';
|
|
4
|
+
import { Button } from '@plone/components';
|
|
5
|
+
import isEqual from 'lodash/isEqual';
|
|
6
|
+
import find from 'lodash/find';
|
|
7
|
+
|
|
8
|
+
export type Actions =
|
|
9
|
+
| {
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
style: Record<`--${string}`, string>;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
style: undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A tuple that has an icon in the first element and a i18n string in the second.
|
|
22
|
+
*/
|
|
23
|
+
export type ActionInfo = [React.ReactElement<any>, string] | [string, string];
|
|
24
|
+
|
|
25
|
+
export type ButtonsWidgetProps = {
|
|
26
|
+
/**
|
|
27
|
+
* Unique identifier for the widget.
|
|
28
|
+
*/
|
|
29
|
+
id: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Callback function to handle changes.
|
|
33
|
+
*/
|
|
34
|
+
onChange: Function;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* List of actions available for the widget.
|
|
38
|
+
*/
|
|
39
|
+
actions: Actions[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map containing additional the information (icon and i18n string) for each action.
|
|
43
|
+
*/
|
|
44
|
+
actionsInfoMap: Record<string, ActionInfo>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List of actions to be filtered out. In case that we don't want the default ones
|
|
48
|
+
* we can filter them out.
|
|
49
|
+
*/
|
|
50
|
+
filterActions: string[];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Current value of the widget.
|
|
54
|
+
*/
|
|
55
|
+
value: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default value of the widget.
|
|
59
|
+
*/
|
|
60
|
+
default: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Indicates if the widget is disabled.
|
|
64
|
+
*/
|
|
65
|
+
disabled: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Indicates if the widget is disabled (alternative flag for compatibility reasons).
|
|
69
|
+
*/
|
|
70
|
+
isDisabled: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ButtonsWidget = (props: ButtonsWidgetProps) => {
|
|
74
|
+
const { disabled, id, onChange, actions, actionsInfoMap, value, isDisabled } =
|
|
75
|
+
props;
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (!props.value && props.default) {
|
|
79
|
+
props.onChange(
|
|
80
|
+
props.id,
|
|
81
|
+
find(actions, { name: props.default })?.style || props.default,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<FormFieldWrapper {...props} className="widget">
|
|
88
|
+
<div className="buttons buttons-widget">
|
|
89
|
+
{actions.map((action) => (
|
|
90
|
+
<Button
|
|
91
|
+
key={action.name}
|
|
92
|
+
aria-label={actionsInfoMap[action.name][1]}
|
|
93
|
+
onPress={() => onChange(id, action.style || action.name)}
|
|
94
|
+
className={
|
|
95
|
+
isEqual(value, action.style || action.name) ? 'active' : null
|
|
96
|
+
}
|
|
97
|
+
isDisabled={disabled || isDisabled}
|
|
98
|
+
>
|
|
99
|
+
{typeof actionsInfoMap[action.name][0] === 'string' ? (
|
|
100
|
+
<div className="image-sizes-text">
|
|
101
|
+
{actionsInfoMap[action.name][0]}
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<Icon
|
|
105
|
+
name={actionsInfoMap[action.name][0]}
|
|
106
|
+
title={actionsInfoMap[action.name][1] || action.name}
|
|
107
|
+
size="24px"
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</Button>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</FormFieldWrapper>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default ButtonsWidget;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { FormattedMessage } from 'react-intl';
|
|
4
|
+
import type { Content } from '@plone/types';
|
|
5
|
+
import cx from 'classnames';
|
|
6
|
+
import config from '@plone/volto/registry';
|
|
7
|
+
|
|
8
|
+
type FormState = {
|
|
9
|
+
content: {
|
|
10
|
+
data: Content;
|
|
11
|
+
};
|
|
12
|
+
form: {
|
|
13
|
+
global: Content;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ColorContrastChecker = (props: { id: string; value: string }) => {
|
|
18
|
+
const { id, value } = props;
|
|
19
|
+
const [ligtherColor, setLigtherColor] = useState('#ffffff');
|
|
20
|
+
const [darkerColor, setDarkerColor] = useState('#000000');
|
|
21
|
+
const [contrastRatio, setContrastRatio] = useState(21);
|
|
22
|
+
|
|
23
|
+
const formData = useSelector<FormState, Content>(
|
|
24
|
+
(state) => state.form.global,
|
|
25
|
+
);
|
|
26
|
+
const colorMap = config.settings.colorMap;
|
|
27
|
+
const colorPair = colorMap[id].colorPair;
|
|
28
|
+
const colorDefault = colorMap[id].default;
|
|
29
|
+
|
|
30
|
+
// Convert hex to RGB
|
|
31
|
+
const hexToRgb = (hex) => {
|
|
32
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
33
|
+
return result
|
|
34
|
+
? {
|
|
35
|
+
r: parseInt(result[1], 16),
|
|
36
|
+
g: parseInt(result[2], 16),
|
|
37
|
+
b: parseInt(result[3], 16),
|
|
38
|
+
}
|
|
39
|
+
: null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Calculate relative luminance
|
|
43
|
+
const getLuminance = (r, g, b) => {
|
|
44
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
45
|
+
c = c / 255;
|
|
46
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
47
|
+
});
|
|
48
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Calculate contrast ratio
|
|
52
|
+
const getContrastRatio = (l1, l2) => {
|
|
53
|
+
const lighter = Math.max(l1, l2);
|
|
54
|
+
const darker = Math.min(l1, l2);
|
|
55
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ligtherColorObject = hexToRgb(ligtherColor);
|
|
59
|
+
const darkerColorObject = hexToRgb(darkerColor);
|
|
60
|
+
|
|
61
|
+
const lcLum = getLuminance(
|
|
62
|
+
ligtherColorObject?.r,
|
|
63
|
+
ligtherColorObject?.g,
|
|
64
|
+
ligtherColorObject?.b,
|
|
65
|
+
);
|
|
66
|
+
const dcLum = getLuminance(
|
|
67
|
+
darkerColorObject?.r,
|
|
68
|
+
darkerColorObject?.g,
|
|
69
|
+
darkerColorObject?.b,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const ratio = getContrastRatio(lcLum, dcLum);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setDarkerColor(value ?? colorDefault);
|
|
76
|
+
setLigtherColor(formData[colorPair] ?? colorMap[colorPair].default);
|
|
77
|
+
setContrastRatio(ratio);
|
|
78
|
+
}, [ratio, value, formData, colorDefault, colorPair, colorMap]);
|
|
79
|
+
|
|
80
|
+
// Get WCAG compliance levels
|
|
81
|
+
const getComplianceLevel = (ratio) => {
|
|
82
|
+
if (ratio >= 3) return 'AA Large';
|
|
83
|
+
return 'Failed';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
{formData[id] && contrastRatio < 4.5 && (
|
|
89
|
+
<span
|
|
90
|
+
className={cx('color-contrast-label')}
|
|
91
|
+
role="alert"
|
|
92
|
+
aria-live="polite"
|
|
93
|
+
>
|
|
94
|
+
<FormattedMessage
|
|
95
|
+
id="ColorContrastCheckerMessage"
|
|
96
|
+
defaultMessage={
|
|
97
|
+
'The color contrast ratio {contrastRatio}:1 might not be accessible for all. WCAG Level: {complianceLevel}'
|
|
98
|
+
}
|
|
99
|
+
values={{
|
|
100
|
+
contrastRatio: contrastRatio.toFixed(1),
|
|
101
|
+
complianceLevel: getComplianceLevel(contrastRatio),
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
<a
|
|
105
|
+
target="_blank"
|
|
106
|
+
href="https://webaim.org/articles/contrast/"
|
|
107
|
+
rel="noreferrer"
|
|
108
|
+
>
|
|
109
|
+
?
|
|
110
|
+
</a>
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default ColorContrastChecker;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
|
|
2
|
+
import { HexColorPicker, HexColorInput } from 'react-colorful';
|
|
3
|
+
import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
|
|
4
|
+
import { ColorSwatch, CloseIcon } from '@plone/components';
|
|
5
|
+
import ColorContrastChecker from './ColorContrastChecker';
|
|
6
|
+
import config from '@plone/volto/registry';
|
|
7
|
+
|
|
8
|
+
const ColorPicker = (props: {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (id: string, value: any) => void;
|
|
13
|
+
}) => {
|
|
14
|
+
const { id, onChange, value } = props;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<FormFieldWrapper {...props} className="theme-color-picker">
|
|
19
|
+
<DialogTrigger>
|
|
20
|
+
<Button className="theme-color-picker-button">
|
|
21
|
+
<ColorSwatch color={value || '#fff'} />
|
|
22
|
+
</Button>
|
|
23
|
+
|
|
24
|
+
<Popover placement="bottom start">
|
|
25
|
+
<Dialog className="theme-color-picker-dialog">
|
|
26
|
+
<HexColorPicker
|
|
27
|
+
color={value || ''}
|
|
28
|
+
onChange={(value) => {
|
|
29
|
+
// edge case for Batman value
|
|
30
|
+
if (value !== '#NaNNaNNaN') {
|
|
31
|
+
onChange(id, value);
|
|
32
|
+
}
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
</Dialog>
|
|
36
|
+
</Popover>
|
|
37
|
+
</DialogTrigger>
|
|
38
|
+
<HexColorInput
|
|
39
|
+
color={value || ''}
|
|
40
|
+
onChange={(value) => onChange(id, value)}
|
|
41
|
+
prefixed
|
|
42
|
+
/>
|
|
43
|
+
<Button
|
|
44
|
+
className="theme-color-picker-reset react-aria-Button"
|
|
45
|
+
onPress={() => {
|
|
46
|
+
onChange(id, '');
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<CloseIcon size="S" />
|
|
50
|
+
</Button>
|
|
51
|
+
</FormFieldWrapper>
|
|
52
|
+
{config.settings.colorMap[props.id] && (
|
|
53
|
+
<ColorContrastChecker {...props} />
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default ColorPicker;
|
|
@@ -2,7 +2,7 @@ import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWr
|
|
|
2
2
|
import { Button } from '@plone/components';
|
|
3
3
|
import cx from 'classnames';
|
|
4
4
|
|
|
5
|
-
type Color =
|
|
5
|
+
export type Color =
|
|
6
6
|
| {
|
|
7
7
|
name: string;
|
|
8
8
|
label: string;
|
|
@@ -14,7 +14,7 @@ type Color =
|
|
|
14
14
|
style: undefined;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export type
|
|
17
|
+
export type ColorSwatchProps = {
|
|
18
18
|
id: string;
|
|
19
19
|
title: string;
|
|
20
20
|
value?: string;
|
|
@@ -27,12 +27,12 @@ export type ColorPickerWidgetProps = {
|
|
|
27
27
|
themes: Color[];
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const ColorSwatch = (props: ColorSwatchProps) => {
|
|
31
31
|
const { id, value, onChange } = props;
|
|
32
32
|
const colors = props.themes || props.colors || [];
|
|
33
33
|
|
|
34
34
|
return colors.length > 0 ? (
|
|
35
|
-
<FormFieldWrapper {...props} className="
|
|
35
|
+
<FormFieldWrapper {...props} className="color-swatch-widget">
|
|
36
36
|
<div className="buttons">
|
|
37
37
|
{colors.map((color) => {
|
|
38
38
|
const colorName = color.name;
|
|
@@ -57,4 +57,4 @@ const ColorPickerWidget = (props: ColorPickerWidgetProps) => {
|
|
|
57
57
|
) : null;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
export default
|
|
60
|
+
export default ColorSwatch;
|