feat: v4.0.0 (#1016)

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
Co-authored-by: Mathieu COSYNS <64072917+Mathieu-COSYNS@users.noreply.github.com>
This commit is contained in:
Daniel Lautzenheiser
2024-01-03 15:14:09 -05:00
committed by GitHub
parent 682576ffc4
commit 799c7e6936
732 changed files with 48477 additions and 10886 deletions

View File

@ -0,0 +1,23 @@
.CMS_WidgetBoolean_content {
@apply flex
gap-2
items-center;
}
.CMS_WidgetBoolean_prefix {
color: var(--text-secondary);
@apply text-sm
whitespace-nowrap;
line-height: 100%;
}
.CMS_WidgetBoolean_suffix {
color: var(--text-secondary);
@apply text-sm
whitespace-nowrap;
line-height: 100%;
}

View File

@ -3,11 +3,14 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import Field from '@staticcms/core/components/common/field/Field';
import Switch from '@staticcms/core/components/common/switch/Switch';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { BooleanField, WidgetControlProps } from '@staticcms/core/interface';
import type { BooleanField, WidgetControlProps } from '@staticcms/core';
import type { ChangeEvent, FC } from 'react';
import './BooleanControl.css';
const classes = generateClassNames('WidgetBoolean', [
'root',
'error',
@ -15,6 +18,9 @@ const classes = generateClassNames('WidgetBoolean', [
'disabled',
'for-single-list',
'input',
'content',
'prefix',
'suffix',
]);
const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
@ -43,6 +49,9 @@ const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
[onChange],
);
const prefix = useMemo(() => field.prefix ?? '', [field.prefix]);
const suffix = useMemo(() => field.suffix ?? '', [field.suffix]);
return (
<Field
inputRef={ref}
@ -61,13 +70,17 @@ const BooleanControl: FC<WidgetControlProps<boolean, BooleanField>> = ({
forSingleList && classes['for-single-list'],
)}
>
<Switch
ref={ref}
value={internalValue}
disabled={disabled}
onChange={handleChange}
rootClassName={classes.input}
/>
<div className={classes.content}>
{isNotEmpty(prefix) ? <div className={classes.prefix}>{prefix}</div> : null}
<Switch
ref={ref}
value={internalValue}
disabled={disabled}
onChange={handleChange}
rootClassName={classes.input}
/>
{isNotEmpty(suffix) ? <div className={classes.suffix}>{suffix}</div> : null}
</div>
</Field>
);
};

View File

@ -3,7 +3,7 @@
*/
import '@testing-library/jest-dom';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { mockBooleanField } from '@staticcms/test/data/fields.mock';
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';

View File

@ -1,7 +1,7 @@
import controlComponent from './BooleanControl';
import schema from './schema';
import type { BooleanField, WidgetParam } from '@staticcms/core/interface';
import type { BooleanField, WidgetParam } from '@staticcms/core';
const BooleanWidget = (): WidgetParam<boolean, BooleanField> => {
return {

View File

@ -1,5 +1,7 @@
export default {
properties: {
default: { type: 'boolean' },
prefix: { type: 'string' },
suffix: { type: 'string' },
},
};

View File

@ -1,11 +1,10 @@
.CMS_WidgetCode_root {
border-color: var(--background-light);
@apply relative
flex
flex-col
border-b
border-slate-400
focus-within:border-blue-800
dark:focus-within:border-blue-100;
border-b;
&.CMS_WidgetCode_for-single-list {
& .CMS_WidgetCode_field-wrapper {
@ -19,8 +18,7 @@
}
& .CMS_WidgetCode_expand-button-icon {
@apply text-slate-300
dark:text-slate-600;
color: var(--text-secondary);
}
}
@ -34,17 +32,27 @@
&:not(.CMS_WidgetCode_error) {
&:not(.CMS_WidgetCode_disabled) {
&:hover,
&:focus {
& .CMS_WidgetCode_label {
@apply text-blue-500;
}
&:focus-within {
border-color: var(--primary-main);
& .CMS_WidgetCode_expand-button-icon {
@apply text-blue-500;
& .CMS_WidgetCode_label {
color: var(--primary-main);
}
}
}
}
&.CMS_WidgetCode_error {
&:not(.CMS_WidgetCode_disabled) {
& .CMS_WidgetCode_label {
color: var(--error-main);
}
&:focus-within {
border-color: var(--error-main);
}
}
}
}
.CMS_WidgetCode_field-wrapper {
@ -66,7 +74,6 @@
focus:outline-none
focus-visible:ring
gap-2
focus-visible:ring-opacity-75
items-center;
}

View File

@ -7,21 +7,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import ErrorMessage from '@staticcms/core/components/common/field/ErrorMessage';
import Hint from '@staticcms/core/components/common/field/Hint';
import Label from '@staticcms/core/components/common/field/Label';
import useTheme from '@staticcms/core/components/theme/hooks/useTheme';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import { selectTheme } from '@staticcms/core/reducers/selectors/globalUI';
import { useAppSelector } from '@staticcms/core/store/hooks';
import SettingsButton from './SettingsButton';
import SettingsPane from './SettingsPane';
import languages from './data/languages';
import type {
CodeField,
ProcessedCodeLanguage,
WidgetControlProps,
} from '@staticcms/core/interface';
import type { CodeField, ProcessedCodeLanguage, WidgetControlProps } from '@staticcms/core';
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
import type { FC, MouseEvent } from 'react';
@ -62,7 +57,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
errors,
disabled,
}) => {
const theme = useAppSelector(selectTheme);
const theme = useTheme();
const keys = useMemo(() => {
const defaults = {
@ -179,7 +174,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
return (
<div
data-testid="list-field"
data-testid={`code-field-${label}`}
className={classNames(
classes.root,
disabled && classes.disabled,
@ -203,7 +198,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
variant="inline"
disabled={disabled}
>
{label}
{label.trim()}
</Label>
{open && allowLanguageSelection ? (
<SettingsButton onClick={toggleSettings} disabled={disabled} />
@ -230,7 +225,7 @@ const CodeControl: FC<WidgetControlProps<string | { [key: string]: string }, Cod
editable={true}
onChange={handleChange}
extensions={extensions}
theme={theme}
theme={theme.codemirror.theme}
readOnly={disabled}
/>
</div>

View File

@ -3,7 +3,7 @@ import React from 'react';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { CodeField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { CodeField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
const classes = generateClassNames('WidgetCodePreview', ['root']);

View File

@ -1,7 +1,2 @@
.CMS_WidgetCode_SettingsButton_root {
}
.CMS_WidgetCode_SettingsButton_icon {
@apply w-5
h-5;
}

View File

@ -9,7 +9,7 @@ import type { FC, MouseEvent } from 'react';
import './SettingsButton.css';
const classes = generateClassNames('WidgetCode_SettingsButton', ['root', 'icon']);
const classes = generateClassNames('WidgetCode_SettingsButton', ['root']);
export interface SettingsButtonProps {
showClose?: boolean;
@ -20,18 +20,15 @@ export interface SettingsButtonProps {
const SettingsButton: FC<SettingsButtonProps> = ({ showClose = false, disabled, onClick }) => {
return (
<IconButton
icon={showClose ? CloseIcon : SettingsIcon}
onClick={onClick}
size="small"
color="secondary"
variant="text"
disabled={disabled}
className={classes.root}
>
{showClose ? (
<CloseIcon className={classes.icon} />
) : (
<SettingsIcon className={classes.icon} />
)}
</IconButton>
rootClassName={classes.root}
aria-label="toggle settings"
/>
);
};

View File

@ -1,6 +1,8 @@
.CMS_WidgetCodeSettings_root {
background: var(--background-light);
@apply absolute
top-10
top-[41px]
bottom-0
right-0
w-40
@ -8,12 +10,5 @@
flex-col
gap-2
z-10
shadow-sm
bg-gray-100
dark:bg-slate-800
border-l
border-l-slate-400
border-t
border-t-slate-300
dark:border-t-slate-700;
shadow-sm;
}

View File

@ -1,6 +1,7 @@
import isHotkey from 'is-hotkey';
import React from 'react';
import { useTranslate } from '@staticcms/core/lib';
import Label from '@staticcms/core/components/common/field/Label';
import Select from '@staticcms/core/components/common/select/Select';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
@ -80,12 +81,14 @@ const SettingsPane: FC<SettingsPaneProps> = ({
language,
onChangeLanguage,
}) => {
const t = useTranslate();
return (
<div onKeyDown={e => isHotkey('esc', e) && hideSettings()} className={classes.root}>
<SettingsSelect
type="language"
label="Language"
placeholder="Select language"
label={t('editor.editorWidgets.code.language')}
placeholder={t('editor.editorWidgets.code.selectLanguage')}
uniqueId={uniqueId}
value={language}
options={languages}

View File

@ -1,4 +1,4 @@
import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
import type { ProcessedCodeLanguage } from '@staticcms/core';
const languages: ProcessedCodeLanguage[] = [
{

View File

@ -2,7 +2,7 @@ import controlComponent from './CodeControl';
import previewComponent from './CodePreview';
import schema from './schema';
import type { CodeField, WidgetParam } from '@staticcms/core/interface';
import type { CodeField, WidgetParam } from '@staticcms/core';
const CodeWidget = (): WidgetParam<string | { [key: string]: string }, CodeField> => {
return {

View File

@ -3,7 +3,7 @@ import yaml from 'js-yaml';
import uniq from 'lodash/uniq';
import path from 'path';
import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
import type { ProcessedCodeLanguage } from '@staticcms/core';
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
const rawDataPath = '../data/languages-raw.yml';
@ -26,7 +26,7 @@ function outputData(data: ProcessedCodeLanguage[]) {
const filePath = path.resolve(__dirname, outputPath);
return fs.writeFile(
filePath,
`import type { ProcessedCodeLanguage } from '@staticcms/core/interface';
`import type { ProcessedCodeLanguage } from '@staticcms/core';
const languages: ProcessedCodeLanguage[] = ${JSON.stringify(data, null, 2)};

View File

@ -17,7 +17,8 @@
}
.CMS_WidgetColor_content {
@apply flex
@apply relative
flex
items-center
pt-2
px-3
@ -38,7 +39,7 @@
.CMS_WidgetColor_color-picker-wrapper {
@apply absolute
bottom-0;
-bottom-0.5;
}
.CMS_WidgetColor_color-picker-backdrop {
@ -50,7 +51,7 @@
.CMS_WidgetColor_color-picker {
@apply absolute
z-20
-top-3;
top-0;
}
.CMS_WidgetColor_input {
@ -58,8 +59,3 @@
.CMS_WidgetColor_clear-button {
}
.CMS_WidgetColor_clear-button-icon {
@apply w-5
h-5;
}

View File

@ -9,7 +9,7 @@ import TextField from '@staticcms/core/components/common/text-field/TextField';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { ColorField, WidgetControlProps } from '@staticcms/core/interface';
import type { ColorField, WidgetControlProps } from '@staticcms/core';
import type { ChangeEvent, FC, MouseEvent, MouseEventHandler } from 'react';
import type { ColorResult } from 'react-color';
@ -30,7 +30,6 @@ export const classes = generateClassNames('WidgetColor', [
'color-picker',
'input',
'clear-button',
'clear-button-icon',
]);
const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
@ -164,13 +163,13 @@ const ColorControl: FC<WidgetControlProps<string, ColorField>> = ({
/>
{showClearButton ? (
<IconButton
icon={CloseIcon}
variant="text"
onClick={handleClear}
disabled={disabled}
className={classes['clear-button']}
>
<CloseIcon className={classes['clear-button-icon']} />
</IconButton>
rootClassName={classes['clear-button']}
aria-label="clear"
/>
) : null}
</div>
</Field>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { ColorField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { ColorField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
const classes = generateClassNames('WidgetColorPreview', ['root']);

View File

@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { act } from '@testing-library/react';
import { mockColorField } from '@staticcms/test/data/fields.mock';
@ -196,7 +196,7 @@ describe(ColorControl.name, () => {
expect(input).not.toHaveFocus();
await act(async () => {
const field = getByTestId('field');
const field = getByTestId('field-Mock Widget');
await userEvent.click(field);
});

View File

@ -3,7 +3,7 @@ import previewComponent from './ColorPreview';
import schema from './schema';
import validator from './validator';
import type { ColorField, WidgetParam } from '@staticcms/core/interface';
import type { ColorField, WidgetParam } from '@staticcms/core';
const ColorWidget = (): WidgetParam<string, ColorField> => {
return {

View File

@ -2,7 +2,7 @@ import validateColor from 'validate-color';
import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes';
import type { ColorField, FieldValidationMethod } from '@staticcms/core/interface';
import type { ColorField, FieldValidationMethod } from '@staticcms/core';
const validator: FieldValidationMethod<string, ColorField> = ({ value, t }) => {
if (typeof value !== 'string') {

View File

@ -2,15 +2,49 @@
}
.CMS_WidgetDateTime_wrapper {
@apply !w-date-widget;
}
.CMS_WidgetDateTime_date-input {
.CMS_WidgetDateTime_date {
@apply flex-grow;
}
.CMS_WidgetDateTime_time-input {
.CMS_WidgetDateTime_time {
@apply flex-grow;
}
.CMS_WidgetDateTime_datetime-input {
@apply truncate;
.CMS_WidgetDateTime_datetime {
}
.CMS_WidgetDateTime_inputs {
@apply flex
items-center
w-full
ps-1.5
pe-2.5
gap-2;
& .CMS_WidgetDateTime_input-wrapper {
@apply flex-grow;
& .CMS_WidgetDateTime_input {
@apply py-1
pl-2;
}
& .MuiOutlinedInput-root {
& .MuiOutlinedInput-notchedOutline {
@apply border-0;
}
&.Mui-focused {
& .MuiOutlinedInput-notchedOutline {
@apply border-0;
}
}
}
& .MuiIconButton-root {
color: var(--text-secondary);
}
}
}

View File

@ -1,30 +1,23 @@
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { MobileDatePicker } from '@mui/x-date-pickers/MobileDatePicker';
import { MobileDateTimePicker } from '@mui/x-date-pickers/MobileDateTimePicker';
import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker';
import { TimePicker } from '@mui/x-date-pickers/TimePicker';
import formatDate from 'date-fns/format';
import parse from 'date-fns/parse';
import parseISO from 'date-fns/parseISO';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import Field from '@staticcms/core/components/common/field/Field';
import TextField from '@staticcms/core/components/common/text-field/TextField';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import NowButton from './components/NowButton';
import {
DEFAULT_DATETIME_FORMAT,
DEFAULT_DATE_FORMAT,
DEFAULT_TIMEZONE_FORMAT,
DEFAULT_TIME_FORMAT,
} from './constants';
import { DEFAULT_DATETIME_FORMAT } from './constants';
import { useDatetimeFormats } from './datetime.util';
import { localToUTC } from './utc.util';
import type { TextFieldProps as MuiTextFieldProps } from '@mui/material/TextField';
import type { TextFieldProps } from '@staticcms/core/components/common/text-field/TextField';
import type { DateTimeField, WidgetControlProps } from '@staticcms/core/interface';
import type { DateTimeField, WidgetControlProps } from '@staticcms/core';
import type { FC } from 'react';
import './DateTimeControl.css';
@ -36,28 +29,11 @@ export const classes = generateClassNames('WidgetDateTime', [
'disabled',
'for-single-list',
'wrapper',
'date-input',
'time-input',
'datetime-input',
'inputs',
'input-wrapper',
'input',
]);
function convertMuiTextFieldProps({
inputProps,
disabled,
onClick,
}: MuiTextFieldProps): TextFieldProps {
const value: string = inputProps?.value ?? '';
return {
type: 'text',
value,
disabled,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange: () => {},
onClick,
};
}
const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
field,
label,
@ -67,7 +43,6 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
errors,
hasErrors,
forSingleList,
t,
onChange,
}) => {
const ref = useRef<HTMLInputElement | null>(null);
@ -80,72 +55,16 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
setOpen(false);
}, []);
const timezoneExtra = useMemo(
() => (field.picker_utc ? '' : DEFAULT_TIMEZONE_FORMAT),
[field.picker_utc],
);
const { format, dateFormat, timeFormat } = useMemo(() => {
// dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
const dateFormat: string | boolean = field.date_format ?? true;
// show time-picker? false hides it, true shows it using default format
const timeFormat: string | boolean = field.time_format ?? true;
let finalFormat = field.format;
if (timeFormat === false) {
finalFormat = field.format ?? DEFAULT_DATE_FORMAT;
} else if (dateFormat === false) {
finalFormat = field.format ?? `${DEFAULT_TIME_FORMAT}${timezoneExtra}`;
} else {
finalFormat = field.format ?? `${DEFAULT_DATETIME_FORMAT}${timezoneExtra}`;
}
return {
format: finalFormat,
dateFormat,
timeFormat,
};
}, [field.date_format, field.format, field.time_format, timezoneExtra]);
const inputFormat = useMemo(() => {
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
formatParts.push(dateFormat);
} else if (dateFormat !== false) {
formatParts.push(DEFAULT_DATE_FORMAT);
}
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
formatParts.push(timeFormat);
} else if (timeFormat !== false) {
formatParts.push(`${DEFAULT_TIME_FORMAT}${timezoneExtra}`);
}
if (formatParts.length > 0) {
return formatParts.join(' ');
}
}
if (timeFormat === false) {
return format ?? DEFAULT_DATE_FORMAT;
}
if (dateFormat === false) {
return format ?? `${DEFAULT_TIME_FORMAT}${timezoneExtra}`;
}
return format ?? `${DEFAULT_DATETIME_FORMAT}${timezoneExtra}`;
}, [dateFormat, format, timeFormat, timezoneExtra]);
const { storageFormat, dateFormat, timeFormat, displayFormat } = useDatetimeFormats(field);
const defaultValue = useMemo(() => {
const today = field.picker_utc ? localToUTC(new Date()) : new Date();
return field.default === undefined
? format
? formatDate(today, format)
? storageFormat
? formatDate(today, storageFormat)
: formatDate(today, DEFAULT_DATETIME_FORMAT)
: field.default;
}, [field.default, field.picker_utc, format]);
}, [field.default, field.picker_utc, storageFormat]);
const [internalRawValue, setInternalValue] = useState(value);
const internalValue = useMemo(
@ -163,12 +82,12 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
return valueToParse;
}
return format ? parse(valueToParse, format, new Date()) : parseISO(valueToParse);
}, [defaultValue, format, internalValue]);
return storageFormat ? parse(valueToParse, storageFormat, new Date()) : parseISO(valueToParse);
}, [defaultValue, storageFormat, internalValue]);
const handleChange = useCallback(
(datetime: Date | null) => {
if (datetime === null) {
if (datetime === null || isNaN(datetime.getTime())) {
setInternalValue(defaultValue);
onChange(defaultValue);
return;
@ -176,121 +95,96 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
const adjustedValue = field.picker_utc ? localToUTC(datetime) : datetime;
const formattedValue = formatDate(adjustedValue, format);
const formattedValue = formatDate(adjustedValue, storageFormat);
setInternalValue(formattedValue);
onChange(formattedValue);
},
[defaultValue, field.picker_utc, format, onChange],
[defaultValue, field.picker_utc, storageFormat, onChange],
);
const inputRef = useRef<HTMLInputElement>();
const rootRef = useForkRef(ref, inputRef);
const dateTimePicker = useMemo(() => {
if (dateFormat && !timeFormat) {
return (
<MobileDatePicker
key="mobile-date-picker"
inputFormat={inputFormat}
label={label}
<DatePicker
key="date-picker"
format={displayFormat}
value={dateValue}
disabled={disabled}
onChange={handleChange}
onOpen={handleOpen}
onClose={handleClose}
renderInput={props => (
<>
<TextField
key="mobile-date-input"
data-testid="date-input"
{...convertMuiTextFieldProps(props)}
inputRef={ref}
cursor="pointer"
inputClassName={classes['date-input']}
/>
<NowButton
key="mobile-date-now"
t={t}
handleChange={v => handleChange(v)}
disabled={disabled}
/>
</>
)}
className={classes['input-wrapper']}
inputRef={rootRef}
slotProps={{
textField: {
inputProps: {
'data-testid': 'date-input',
className: classes.input,
},
},
}}
/>
);
}
if (!dateFormat && timeFormat) {
return (
<MobileTimePicker
<TimePicker
key="time-picker"
label={label}
inputFormat={inputFormat}
format={displayFormat}
value={dateValue}
disabled={disabled}
onChange={handleChange}
onOpen={handleOpen}
onClose={handleClose}
renderInput={props => (
<>
<TextField
key="mobile-time-input"
data-testid="time-input"
{...convertMuiTextFieldProps(props)}
inputRef={ref}
cursor="pointer"
inputClassName={classes['time-input']}
/>
<NowButton
key="mobile-date-now"
t={t}
handleChange={v => handleChange(v)}
disabled={disabled}
/>
</>
)}
className={classes['input-wrapper']}
inputRef={rootRef}
slotProps={{
textField: {
inputProps: {
'data-testid': 'time-input',
className: classes.input,
},
},
}}
/>
);
}
return (
<MobileDateTimePicker
key="mobile-date-time-picker"
inputFormat={inputFormat}
label={label}
<DateTimePicker
key="date-time-picker"
format={displayFormat}
value={dateValue}
disabled={disabled}
onChange={handleChange}
onOpen={handleOpen}
onClose={handleClose}
renderInput={props => (
<>
<TextField
key="mobile-date-time-input"
data-testid="date-time-input"
{...convertMuiTextFieldProps(props)}
inputRef={ref}
cursor="pointer"
inputClassName={classes['datetime-input']}
/>
<NowButton
key="mobile-date-now"
t={t}
handleChange={v => handleChange(v)}
disabled={disabled}
/>
</>
)}
className={classes['input-wrapper']}
inputRef={rootRef}
slotProps={{
textField: {
inputProps: {
'data-testid': 'date-time-input',
className: classes.input,
},
},
}}
/>
);
}, [
dateFormat,
dateValue,
handleChange,
handleClose,
handleOpen,
inputFormat,
disabled,
label,
t,
timeFormat,
displayFormat,
dateValue,
disabled,
handleChange,
handleOpen,
handleClose,
rootRef,
]);
return (
@ -300,7 +194,7 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
errors={errors}
hint={field.hint}
forSingleList={forSingleList}
cursor="pointer"
cursor="text"
disabled={disabled}
rootClassName={classNames(
classes.root,
@ -311,9 +205,17 @@ const DateTimeControl: FC<WidgetControlProps<string | Date, DateTimeField>> = ({
)}
wrapperClassName={classes.wrapper}
>
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
{dateTimePicker}
</LocalizationProvider>
<div className={classes['inputs']}>
<LocalizationProvider key="localization-provider" dateAdapter={AdapterDateFns}>
{dateTimePicker}
</LocalizationProvider>
<NowButton
key="date-now"
field={field}
handleChange={v => handleChange(v)}
disabled={disabled}
/>
</div>
</Field>
);
};

View File

@ -2,7 +2,7 @@ import React from 'react';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { DateTimeField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { DateTimeField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
const classes = generateClassNames('WidgetDateTimePreview', ['root']);

View File

@ -4,7 +4,7 @@
import { fireEvent, getByText } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { mockDateField, mockDateTimeField, mockTimeField } from '@staticcms/test/data/fields.mock';
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
@ -85,8 +85,7 @@ async function selectDate(
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await act(async () => {
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
await userEventActions.click(input);
});
@ -102,8 +101,7 @@ async function selectTime(
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await act(async () => {
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
await userEventActions.click(input);
});
@ -120,8 +118,7 @@ async function selectDateTime(
const userEventActions = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
await act(async () => {
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
await userEventActions.click(input);
});
@ -169,9 +166,8 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label' });
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12T10:15:35');
});
it('should use default if provided', () => {
@ -179,46 +175,42 @@ describe(DateTimeControl.name, () => {
label: 'I am a label',
field: {
...mockDateTimeField,
default: '2023-01-10T06:23:15.000-10:00',
default: '2023-01-10T06:23:15-10:00',
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-01-10T06:23:15.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-01-10T06:23:15');
});
it('should only use prop value as initial value', async () => {
const { rerender, getByTestId } = renderControl({ value: '2023-02-12T10:15:35.000-10:00' });
const { rerender, getByTestId } = renderControl({ value: '2023-02-12T10:15:35-10:00' });
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12T10:15:35');
rerender({ value: '2023-02-18T14:37:02.000-10:00' });
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
rerender({ value: '2023-02-18T14:37:02-10:00' });
expect(input).toHaveValue('2023-02-12T10:15:35');
});
it('should use prop value exclusively if field is i18n duplicate', async () => {
const { rerender, getByTestId } = renderControl({
field: { ...mockDateTimeField, i18n: 'duplicate' },
duplicate: true,
value: '2023-02-12T10:15:35.000-10:00',
value: '2023-02-12T10:15:35-10:00',
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12T10:15:35');
rerender({ value: '2023-02-18T14:37:02.000-10:00' });
expect(input).toHaveValue('2023-02-18T14:37:02.000-10:00');
rerender({ value: '2023-02-18T14:37:02-10:00' });
expect(input).toHaveValue('2023-02-18T14:37:02');
});
it('should disable input and now button if disabled', () => {
const { getByTestId } = renderControl({ label: 'I am a label', disabled: true });
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).toBeDisabled();
const nowButton = getByTestId('datetime-now');
@ -228,12 +220,11 @@ describe(DateTimeControl.name, () => {
it('should focus current date in modal on field click', async () => {
const { getByTestId } = renderControl();
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).not.toHaveFocus();
await act(async () => {
const field = getByTestId('field');
const field = getByTestId('field-Mock Widget');
await userEventActions.click(field);
});
@ -249,8 +240,7 @@ describe(DateTimeControl.name, () => {
expect(onChange).not.toHaveBeenCalled();
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
await act(async () => {
await userEventActions.click(input);
@ -266,7 +256,7 @@ describe(DateTimeControl.name, () => {
await userEventActions.click(days[0]);
});
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T10:15:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T10:15:35-10:00');
const hours = document.querySelectorAll('.MuiClockNumber-root');
expect(hours.length).toBe(12);
@ -281,7 +271,7 @@ describe(DateTimeControl.name, () => {
fireEvent.touchEnd(square!, hourClockEvent);
});
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:15:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:15:35-10:00');
const minutes = document.querySelectorAll('.MuiClockNumber-root');
expect(minutes.length).toBe(12);
@ -293,7 +283,7 @@ describe(DateTimeControl.name, () => {
fireEvent.touchEnd(square!, minuteClockEvent);
});
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:05:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T01:05:35-10:00');
});
it('should set value to current date and time when now button is clicked', async () => {
@ -304,22 +294,21 @@ describe(DateTimeControl.name, () => {
label: 'I am a label',
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-02-12T10:15:35.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12T10:15:35');
await selectDateTime(getByTestId, 1, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
expect(input).toHaveValue('2023-02-01T02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
expect(input).toHaveValue('2023-02-01T02:20:35');
await act(async () => {
const nowButton = getByTestId('datetime-now');
await userEventActions.click(nowButton);
});
expect(onChange).toHaveBeenLastCalledWith('2023-02-12T10:15:36.000-10:00'); // Testing framework moves the time forward by a second by this point
expect(input).toHaveValue('2023-02-12T10:15:36.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-12T10:15:36-10:00'); // Testing framework moves the time forward by a second by this point
expect(input).toHaveValue('2023-02-12T10:15:36');
});
describe('format', () => {
@ -335,14 +324,13 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('02/12/2023 10:15:35.000-10:00');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('02/12/2023 10:15:35');
await selectDateTime(getByTestId, 1, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
expect(input).toHaveValue('02/01/2023 02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
expect(input).toHaveValue('02/01/2023 02:20:35');
});
it('uses custom time display format', async () => {
@ -357,13 +345,12 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12 10:15 am');
await selectDateTime(getByTestId, 1, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
expect(input).toHaveValue('2023-02-01 02:20 am');
});
@ -380,13 +367,12 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).toHaveValue('02/12/2023 10:15 am');
await selectDateTime(getByTestId, 1, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('2023-02-01T02:20:35-10:00');
expect(input).toHaveValue('02/01/2023 02:20 am');
});
@ -404,8 +390,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).toHaveValue('02/12/2023 10:15 am');
await selectDateTime(getByTestId, 1, 3, 20, 'pm');
@ -426,8 +411,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12 10:15');
await selectDateTime(getByTestId, 1, 3, 20, 'pm');
@ -446,9 +430,8 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-02-12T20:15:35.000');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-02-12T20:15:35');
});
it('should use default if provided (assuming default is already in UTC)', () => {
@ -456,13 +439,12 @@ describe(DateTimeControl.name, () => {
label: 'I am a label',
field: {
...utcField,
default: '2023-01-10T06:23:15.000',
default: '2023-01-10T06:23:15',
},
});
const inputWrapper = getByTestId('date-time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('2023-01-10T06:23:15.000');
const input = getByTestId('date-time-input');
expect(input).toHaveValue('2023-01-10T06:23:15');
});
});
});
@ -471,8 +453,7 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label', field: mockDateField });
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
});
@ -485,8 +466,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-01-10');
});
@ -496,8 +476,7 @@ describe(DateTimeControl.name, () => {
value: '2023-02-12',
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
rerender({ value: '2023-02-18' });
@ -511,8 +490,7 @@ describe(DateTimeControl.name, () => {
value: '2023-02-12',
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
rerender({ value: '2023-02-18' });
@ -526,8 +504,7 @@ describe(DateTimeControl.name, () => {
disabled: true,
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toBeDisabled();
const nowButton = getByTestId('datetime-now');
@ -537,12 +514,11 @@ describe(DateTimeControl.name, () => {
it('should focus current date in modal on field click', async () => {
const { getByTestId } = renderControl({ field: mockDateField });
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).not.toHaveFocus();
await act(async () => {
const field = getByTestId('field');
const field = getByTestId('field-Mock Widget');
await userEventActions.click(field);
});
@ -558,8 +534,7 @@ describe(DateTimeControl.name, () => {
expect(onChange).not.toHaveBeenCalled();
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
await act(async () => {
await userEventActions.click(input);
@ -587,8 +562,7 @@ describe(DateTimeControl.name, () => {
field: mockDateField,
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
await selectDate(getByTestId, 1);
@ -618,8 +592,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('02/12/2023');
await selectDate(getByTestId, 1);
@ -641,8 +614,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('02/12/2023');
await selectDate(getByTestId, 1);
@ -663,8 +635,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
await selectDate(getByTestId, 1);
@ -683,8 +654,7 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-02-12');
});
@ -697,8 +667,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('date-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('date-input');
expect(input).toHaveValue('2023-01-10');
});
});
@ -708,9 +677,8 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label', field: mockTimeField });
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('10:15:35.000-10:00');
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15:35');
});
it('should use default if provided', () => {
@ -718,43 +686,40 @@ describe(DateTimeControl.name, () => {
label: 'I am a label',
field: {
...mockTimeField,
default: '06:23:15.000-10:00',
default: '06:23:15-10:00',
},
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('06:23:15.000-10:00');
const input = getByTestId('time-input');
expect(input).toHaveValue('06:23:15');
});
it('should only use prop value as initial value', async () => {
const { rerender, getByTestId } = renderControl({
field: mockTimeField,
value: '10:15:35.000-10:00',
value: '10:15:35-10:00',
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('10:15:35.000-10:00');
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15:35');
rerender({ value: '14:37:02.000-10:00' });
expect(input).toHaveValue('10:15:35.000-10:00');
rerender({ value: '14:37:02-10:00' });
expect(input).toHaveValue('10:15:35');
});
it('should use prop value exclusively if field is i18n duplicate', async () => {
const { rerender, getByTestId } = renderControl({
field: { ...mockTimeField, i18n: 'duplicate' },
duplicate: true,
value: '10:15:35.000-10:00',
value: '10:15:35-10:00',
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15:35.000-10:00');
expect(input).toHaveValue('10:15:35');
rerender({ value: '14:37:02.000-10:00' });
expect(input).toHaveValue('14:37:02.000-10:00');
rerender({ value: '14:37:02-10:00' });
expect(input).toHaveValue('14:37:02');
});
it('should disable input and now button if disabled', () => {
@ -764,8 +729,7 @@ describe(DateTimeControl.name, () => {
disabled: true,
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).toBeDisabled();
@ -776,12 +740,11 @@ describe(DateTimeControl.name, () => {
it('should focus current time in modal on field click', async () => {
const { getByTestId } = renderControl({ field: mockTimeField });
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).not.toHaveFocus();
await act(async () => {
const field = getByTestId('field');
const field = getByTestId('field-Mock Widget');
await userEventActions.click(field);
});
@ -797,8 +760,7 @@ describe(DateTimeControl.name, () => {
expect(onChange).not.toHaveBeenCalled();
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
await act(async () => {
await userEventActions.click(input);
@ -819,7 +781,7 @@ describe(DateTimeControl.name, () => {
fireEvent.touchEnd(square!, hourClockEvent);
});
expect(onChange).toHaveBeenLastCalledWith('01:15:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('01:15:35-10:00');
const minutes = document.querySelectorAll('.MuiClockNumber-root');
expect(minutes.length).toBe(12);
@ -831,7 +793,7 @@ describe(DateTimeControl.name, () => {
fireEvent.touchEnd(square!, minuteClockEvent);
});
expect(onChange).toHaveBeenLastCalledWith('01:05:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('01:05:35-10:00');
});
it('should set value to current time when now button is clicked', async () => {
@ -843,22 +805,21 @@ describe(DateTimeControl.name, () => {
field: mockTimeField,
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('10:15:35.000-10:00');
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15:35');
await selectTime(getByTestId, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('02:20:35.000-10:00');
expect(input).toHaveValue('02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('02:20:35-10:00');
expect(input).toHaveValue('02:20:35');
await act(async () => {
const nowButton = getByTestId('datetime-now');
await userEventActions.click(nowButton);
});
expect(onChange).toHaveBeenLastCalledWith('10:15:36.000-10:00'); // Testing framework moves the time forward by a second by this point
expect(input).toHaveValue('10:15:36.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('10:15:36-10:00'); // Testing framework moves the time forward by a second by this point
expect(input).toHaveValue('10:15:36');
});
describe('format', () => {
@ -874,13 +835,12 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15 am');
await selectTime(getByTestId, 2, 20, 'am');
expect(onChange).toHaveBeenLastCalledWith('02:20:35.000-10:00');
expect(onChange).toHaveBeenLastCalledWith('02:20:35-10:00');
expect(input).toHaveValue('02:20 am');
});
@ -897,8 +857,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15 am');
await selectTime(getByTestId, 3, 20, 'pm');
@ -919,8 +878,7 @@ describe(DateTimeControl.name, () => {
},
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
const input = getByTestId('time-input');
expect(input).toHaveValue('10:15');
await selectTime(getByTestId, 3, 20, 'pm');
@ -939,9 +897,8 @@ describe(DateTimeControl.name, () => {
it("should default to today's date if no default is provided", () => {
const { getByTestId } = renderControl({ label: 'I am a label', field: utcField });
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('20:15:35.000');
const input = getByTestId('time-input');
expect(input).toHaveValue('20:15:35');
});
it('should use default if provided (assuming default is already in UTC)', () => {
@ -949,13 +906,12 @@ describe(DateTimeControl.name, () => {
label: 'I am a label',
field: {
...utcField,
default: '06:23:15.000',
default: '06:23:15',
},
});
const inputWrapper = getByTestId('time-input');
const input = inputWrapper.getElementsByTagName('input')[0];
expect(input).toHaveValue('06:23:15.000');
const input = getByTestId('time-input');
expect(input).toHaveValue('06:23:15');
});
});
});

View File

@ -12,12 +12,12 @@ describe('DateTime getDefaultValue', () => {
describe('datetime', () => {
it("should use today's date", () => {
expect(getDefaultValue(undefined, mockDateTimeField)).toEqual('2023-02-12T10:15:35.000');
expect(getDefaultValue(undefined, mockDateTimeField)).toEqual('2023-02-12T10:15:35');
});
it('should use provided default', () => {
expect(getDefaultValue('2022-06-18T14:30:01.000', mockDateTimeField)).toEqual(
'2022-06-18T14:30:01.000',
expect(getDefaultValue('2022-06-18T14:30:01', mockDateTimeField)).toEqual(
'2022-06-18T14:30:01',
);
});
});
@ -34,11 +34,11 @@ describe('DateTime getDefaultValue', () => {
describe('time', () => {
it("should use today's date", () => {
expect(getDefaultValue(undefined, mockTimeField)).toEqual('10:15:35.000');
expect(getDefaultValue(undefined, mockTimeField)).toEqual('10:15:35');
});
it('should use provided default', () => {
expect(getDefaultValue('14:30:01.000', mockTimeField)).toEqual('14:30:01.000');
expect(getDefaultValue('14:30:01', mockTimeField)).toEqual('14:30:01');
});
});
});

View File

@ -1,7 +1,4 @@
.CMS_WidgetDateTime_NowButton_root {
@apply absolute
inset-y-1
right-3
flex
@apply flex
items-center;
}

View File

@ -1,9 +1,10 @@
import React, { useCallback } from 'react';
import Button from '@staticcms/core/components/common/button/Button';
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { TranslatedProps } from '@staticcms/core/interface';
import type { DateTimeField } from '@staticcms/core';
import type { FC, MouseEvent } from 'react';
import './NowButton.css';
@ -12,10 +13,13 @@ const classes = generateClassNames('WidgetDateTime_NowButton', ['root', 'button'
export interface NowButtonProps {
handleChange: (value: Date) => void;
field: DateTimeField;
disabled: boolean;
}
const NowButton: FC<TranslatedProps<NowButtonProps>> = ({ disabled, t, handleChange }) => {
const NowButton: FC<NowButtonProps> = ({ disabled, field, handleChange }) => {
const t = useTranslate();
const handleClick = useCallback(
(event: MouseEvent) => {
event.stopPropagation();
@ -31,8 +35,10 @@ const NowButton: FC<TranslatedProps<NowButtonProps>> = ({ disabled, t, handleCha
data-testid="datetime-now"
onClick={handleClick}
disabled={disabled}
color="secondary"
variant="outlined"
className={classes.button}
aria-label={`set ${field.label ?? field.name} to now`}
>
{t('editor.editorWidgets.datetime.now')}
</Button>

View File

@ -1,4 +1,4 @@
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
export const DEFAULT_TIME_FORMAT = 'HH:mm:ss.SSS';
export const DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS";
export const DEFAULT_TIME_FORMAT = 'HH:mm:ss';
export const DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
export const DEFAULT_TIMEZONE_FORMAT = 'XXX';

View File

@ -0,0 +1,89 @@
import { useMemo } from 'react';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import {
DEFAULT_DATETIME_FORMAT,
DEFAULT_DATE_FORMAT,
DEFAULT_TIMEZONE_FORMAT,
DEFAULT_TIME_FORMAT,
} from './constants';
import type { DateTimeField, DateTimeFormats } from '@staticcms/core';
function getDisplayFormat(
dateFormat: string | boolean,
timeFormat: string | boolean,
storageFormat: string,
) {
if (typeof dateFormat === 'string' || typeof timeFormat === 'string') {
const formatParts: string[] = [];
if (typeof dateFormat === 'string' && isNotEmpty(dateFormat)) {
formatParts.push(dateFormat);
} else if (dateFormat !== false) {
formatParts.push(DEFAULT_DATE_FORMAT);
}
if (typeof timeFormat === 'string' && isNotEmpty(timeFormat)) {
formatParts.push(timeFormat);
} else if (timeFormat !== false) {
formatParts.push(`${DEFAULT_TIME_FORMAT}`);
}
if (formatParts.length > 0) {
return formatParts.join(' ');
}
}
if (timeFormat === false) {
return storageFormat ?? DEFAULT_DATE_FORMAT;
}
if (dateFormat === false) {
return storageFormat ?? `${DEFAULT_TIME_FORMAT}`;
}
return storageFormat ?? `${DEFAULT_DATETIME_FORMAT}`;
}
export function getDatetimeFormats(field: DateTimeField): DateTimeFormats;
export function getDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined;
export function getDatetimeFormats(field: DateTimeField | undefined) {
if (!field) {
return undefined;
}
const timezoneExtra = field.picker_utc ? '' : DEFAULT_TIMEZONE_FORMAT;
// dateFormat and timeFormat are strictly for modifying input field with the date/time pickers
const dateFormat: string | boolean = field.date_format ?? true;
// show time-picker? false hides it, true shows it using default format
const timeFormat: string | boolean = field.time_format ?? true;
let storageFormat = field.format;
let shouldAddTimezoneExtra = false;
if (timeFormat === false) {
storageFormat = field.format ?? DEFAULT_DATE_FORMAT;
} else if (dateFormat === false) {
storageFormat = field.format ?? DEFAULT_TIME_FORMAT;
shouldAddTimezoneExtra = !field.format;
} else {
storageFormat = field.format ?? DEFAULT_DATETIME_FORMAT;
shouldAddTimezoneExtra = !field.format;
}
const displayFormat = getDisplayFormat(dateFormat, timeFormat, storageFormat);
return {
storageFormat: `${storageFormat}${shouldAddTimezoneExtra ? timezoneExtra : ''}`,
dateFormat,
timeFormat,
displayFormat,
timezoneExtra,
};
}
export function useDatetimeFormats(field: DateTimeField): DateTimeFormats;
export function useDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined;
export function useDatetimeFormats(field: DateTimeField | undefined): DateTimeFormats | undefined {
return useMemo(() => getDatetimeFormats(field), [field]);
}

View File

@ -4,7 +4,7 @@ import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import { DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT } from './constants';
import { localToUTC } from './utc.util';
import type { DateTimeField, FieldGetDefaultMethod } from '@staticcms/core/interface';
import type { DateTimeField, FieldGetDefaultMethod } from '@staticcms/core';
const getDefaultValue: FieldGetDefaultMethod<string | Date, DateTimeField> = (
defaultValue,

View File

@ -3,7 +3,7 @@ import previewComponent from './DateTimePreview';
import getDefaultValue from './getDefaultValue';
import schema from './schema';
import type { DateTimeField, WidgetParam } from '@staticcms/core/interface';
import type { DateTimeField, WidgetParam } from '@staticcms/core';
const DateTimeWidget = (): WidgetParam<string | Date, DateTimeField> => {
return {

View File

@ -5,7 +5,7 @@
border-transparent;
&.CMS_WidgetFileImage_drag-over-active {
@apply border-blue-500;
border-color: var(--primary-main);
& .CMS_WidgetFileImage_drop-area {
@apply opacity-100;
@ -29,6 +29,9 @@
}
.CMS_WidgetFileImage_drop-area {
color: var(--primary-light);
background: color-mix(in srgb, var(--background-main) 75%, transparent);
@apply absolute
inset-0
flex
@ -36,10 +39,6 @@
justify-center
pointer-events-none
font-bold
text-blue-500
bg-white/75
dark:text-blue-400
dark:bg-slate-800/75
transition-opacity
opacity-0;
}

View File

@ -3,16 +3,16 @@ import React from 'react';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import type {
Collection,
CollectionWithDefaults,
Entry,
FileOrImageField,
WidgetPreviewProps,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { FC } from 'react';
interface FileLinkProps {
value: string;
collection: Collection<FileOrImageField>;
collection: CollectionWithDefaults<FileOrImageField>;
field: FileOrImageField;
entry: Entry;
}

View File

@ -3,22 +3,23 @@
*/
import '@testing-library/jest-dom';
import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { configLoaded } from '@staticcms/core/actions/config';
import { applyDefaults, configLoaded } from '@staticcms/core/actions/config';
import {
insertMedia,
mediaDisplayURLSuccess,
mediaInserted,
} from '@staticcms/core/actions/mediaLibrary';
import { store } from '@staticcms/core/store';
import { createMockCollection } from '@staticcms/test/data/collections.mock';
import { createMockConfig } from '@staticcms/test/data/config.mock';
import { createMockFolderCollection } from '@staticcms/test/data/collections.mock';
import { createNoDefaultsMockConfig } from '@staticcms/test/data/config.mock';
import { mockFileField } from '@staticcms/test/data/fields.mock';
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
import withFileControl from '../withFileControl';
import type { Config, MediaFile } from '@staticcms/core/interface';
import type { Config, ConfigWithDefaults, FileOrImageField, MediaFile } from '@staticcms/core';
import type { WidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
jest.mock('@staticcms/core/backend');
@ -55,21 +56,27 @@ jest.mock('@staticcms/core/lib/hooks/useMediaAsset', () => (url: string) => url)
describe('File Control', () => {
const FileControl = withFileControl();
const collection = createMockCollection({}, mockFileField);
const config = createMockConfig({
const collection = createMockFolderCollection({}, mockFileField);
const originalConfig = createNoDefaultsMockConfig({
collections: [collection],
});
const mockInsertMedia = insertMedia as jest.Mock;
const renderControl = createWidgetControlHarness(
FileControl,
{ field: mockFileField, config },
{ withMediaLibrary: true },
);
let renderControl: WidgetControlHarness<string | string[], FileOrImageField>;
beforeEach(() => {
store.dispatch(configLoaded(config as unknown as Config));
const config = applyDefaults(originalConfig);
renderControl = createWidgetControlHarness(
FileControl,
{ field: mockFileField, config },
{ withMediaLibrary: true },
);
store.dispatch(
configLoaded(config as unknown as ConfigWithDefaults, originalConfig as unknown as Config),
);
store.dispatch(mediaDisplayURLSuccess('12345', 'path/to/file1.txt'));
store.dispatch(mediaDisplayURLSuccess('67890', 'path/to/file2.png'));

View File

@ -5,36 +5,35 @@
}
.CMS_WidgetFileImage_SortableImage_card {
background: var(--background-dark);
border-color: var(--background-main);
@apply w-image-card
h-image-card
rounded-md
shadow-sm
overflow-hidden
cursor-pointer
border
bg-gray-50/75
border-gray-200/75
dark:bg-slate-800
dark:border-slate-600/75;
border;
&:hover {
& .CMS_WidgetFileImage_SortableImage_controls-wrapper {
@apply visible
bg-blue-200/25
dark:bg-blue-400/60;
background: color-mix(in srgb, var(--primary-main) 25%, transparent);
@apply visible;
}
}
}
.CMS_WidgetFileImage_SortableImage_handle {
--tw-ring-color: color-mix(in srgb, var(--primary-light) 50%, transparent);
@apply absolute
inset-0
rounded-md
z-20
overflow-visible
focus:ring-4
focus:ring-gray-200
dark:focus:ring-slate-700
focus-visible:outline-none;
}
@ -48,33 +47,31 @@
}
.CMS_WidgetFileImage_SortableImage_controls {
background: color-mix(in srgb, var(--background-dark) 90%, transparent);
@apply absolute
top-2
right-2
flex
gap-1;
gap-1
rounded;
}
.CMS_WidgetFileImage_SortableImage_replace-button {
@apply text-white
dark:text-white
bg-gray-900/25
dark:hover:text-blue-100
dark:hover:bg-blue-800/80;
color: var(--primary-contrast-color);
background: color-mix(in srgb, var(--primary-dark) 15%, transparent);
&:hover {
color: var(--primary-light);
}
}
.CMS_WidgetFileImage_SortableImage_remove-button {
@apply relative
text-red-400
bg-gray-900/25
dark:hover:text-red-600
dark:hover:bg-red-800/40
z-30;
}
color: var(--error-main);
background: color-mix(in srgb, var(--error-dark) 15%, transparent);
.CMS_WidgetFileImage_SortableImage_button-icon {
@apply w-5
h-5;
@apply relative
z-30;
}
.CMS_WidgetFileImage_SortableImage_content {

View File

@ -8,7 +8,7 @@ import IconButton from '@staticcms/core/components/common/button/IconButton';
import Image from '@staticcms/core/components/common/image/Image';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { Collection, FileOrImageField } from '@staticcms/core/interface';
import type { CollectionWithDefaults, FileOrImageField } from '@staticcms/core';
import type { FC, MouseEventHandler } from 'react';
import './SortableImage.css';
@ -21,7 +21,6 @@ const classes = generateClassNames('WidgetFileImage_SortableImage', [
'controls',
'replace-button',
'remove-button',
'button-icon',
'content',
'image',
]);
@ -29,7 +28,7 @@ const classes = generateClassNames('WidgetFileImage_SortableImage', [
export interface SortableImageProps {
id: string;
itemValue: string;
collection: Collection<FileOrImageField>;
collection: CollectionWithDefaults<FileOrImageField>;
field: FileOrImageField;
onRemove?: MouseEventHandler;
onReplace?: MouseEventHandler;
@ -98,24 +97,24 @@ const SortableImage: FC<SortableImageProps> = ({
<div className={classes.controls}>
{onReplace ? (
<IconButton
icon={CameraAltIcon}
key="replace"
variant="text"
onClick={handleReplace}
className={classes['replace-button']}
>
<CameraAltIcon className={classes['button-icon']} />
</IconButton>
rootClassName={classes['replace-button']}
aria-label="replace image"
/>
) : null}
{onRemove ? (
<IconButton
icon={DeleteIcon}
key="remove"
variant="text"
color="error"
onClick={handleRemove}
className={classes['remove-button']}
>
<DeleteIcon className={classes['button-icon']} />
</IconButton>
rootClassName={classes['remove-button']}
aria-label="remove image"
/>
) : null}
</div>
</div>

View File

@ -4,6 +4,8 @@
}
.CMS_WidgetFileImage_SortableLink_card {
border-color: var(--background-light);
@apply w-full
shadow-sm
overflow-hidden
@ -11,7 +13,6 @@
border-l-2
border-b
border-solid
border-l-slate-400
p-2;
}
@ -27,22 +28,7 @@
gap-1;
}
.CMS_WidgetFileImage_SortableLink_replace-button {
@apply text-white
dark:text-white
dark:hover:text-blue-100
dark:hover:bg-blue-800/80;
}
.CMS_WidgetFileImage_SortableLink_remove-button {
@apply relative
text-red-400
dark:hover:text-red-600
dark:hover:bg-red-800/40
z-30;
}
.CMS_WidgetFileImage_SortableLink_button-icon {
@apply w-5
h-5;
}

View File

@ -18,7 +18,6 @@ const classes = generateClassNames('WidgetFileImage_SortableLink', [
'controls',
'replace-button',
'remove-button',
'button-icon',
]);
const MAX_DISPLAY_LENGTH = 100;
@ -88,24 +87,25 @@ const SortableLink: FC<SortableLinkProps> = ({ id, itemValue, onRemove, onReplac
<div className={classes.controls}>
{onReplace ? (
<IconButton
icon={ModeEditIcon}
key="replace"
color="secondary"
variant="text"
onClick={handleReplace}
className={classes['replace-button']}
>
<ModeEditIcon className={classes['button-icon']} />
</IconButton>
rootClassName={classes['replace-button']}
aria-label="replace link"
/>
) : null}
{onRemove ? (
<IconButton
icon={DeleteIcon}
key="remove"
variant="text"
color="error"
onClick={handleRemove}
className={classes['remove-button']}
>
<DeleteIcon className={classes['button-icon']} />
</IconButton>
rootClassName={classes['remove-button']}
aria-label="remove link"
/>
) : null}
</div>
</div>

View File

@ -3,7 +3,7 @@ import schema from './schema';
import withFileControl, { getValidFileValue } from './withFileControl';
import type { WithFileControlProps } from './withFileControl';
import type { FileOrImageField, WidgetParam } from '@staticcms/core/interface';
import type { FileOrImageField, WidgetParam } from '@staticcms/core';
const controlComponent = withFileControl();

View File

@ -28,7 +28,7 @@ import SortableImage from './components/SortableImage';
import SortableLink from './components/SortableLink';
import type { DragEndEvent } from '@dnd-kit/core';
import type { FileOrImageField, MediaPath, WidgetControlProps } from '@staticcms/core/interface';
import type { FileOrImageField, MediaPath, WidgetControlProps } from '@staticcms/core';
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
import type { FC, MouseEvent } from 'react';
@ -355,7 +355,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
<div key="controls" className={widgetFileImageClasses.actions}>
<Button
buttonRef={uploadButtonRef}
color="primary"
color="secondary"
variant="outlined"
key="upload"
onClick={handleOpenMediaLibrary}
@ -366,7 +366,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
</Button>
{chooseUrl ? (
<Button
color="primary"
color="secondary"
variant="outlined"
key="choose-url"
onClick={handleUrl(subject)}
@ -387,7 +387,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
<div key="controls" className={widgetFileImageClasses.actions}>
<Button
buttonRef={uploadButtonRef}
color="primary"
color="secondary"
variant="outlined"
key="add-replace"
onClick={handleOpenMediaLibrary}
@ -403,7 +403,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
{chooseUrl ? (
allowsMultiple ? (
<Button
color="primary"
color="secondary"
variant="outlined"
key="choose-url"
onClick={handleUrl(subject)}
@ -414,7 +414,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
</Button>
) : (
<Button
color="primary"
color="secondary"
variant="outlined"
key="replace-url"
onClick={handleUrl(subject)}

View File

@ -3,16 +3,16 @@ import React from 'react';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import type {
Collection,
CollectionWithDefaults,
Entry,
FileOrImageField,
WidgetPreviewProps,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { FC } from 'react';
interface ImageAssetProps {
value: string;
collection: Collection<FileOrImageField>;
collection: CollectionWithDefaults<FileOrImageField>;
field: FileOrImageField;
entry: Entry;
}

View File

@ -2,7 +2,7 @@ import withFileControl, { getValidFileValue } from '../file/withFileControl';
import previewComponent from './ImagePreview';
import schema from './schema';
import type { FileOrImageField, WidgetParam } from '@staticcms/core/interface';
import type { FileOrImageField, WidgetParam } from '@staticcms/core';
const controlComponent = withFileControl({ forImage: true });

View File

@ -36,11 +36,6 @@
w-6;
}
.CMS_WidgetKeyValue_delete-button-icon {
@apply h-5
w-5;
}
.CMS_WidgetKeyValue_actions {
@apply px-3
mt-3;

View File

@ -10,7 +10,7 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import { createEmptyPair } from './util';
import type { KeyValueField, WidgetControlProps } from '@staticcms/core/interface';
import type { KeyValueField, WidgetControlProps } from '@staticcms/core';
import type { ChangeEvent, FC, MouseEvent } from 'react';
import type { Pair } from './types';
@ -28,7 +28,6 @@ const classes = generateClassNames('WidgetKeyValue', [
'header-action-cell-content',
'row',
'delete-button',
'delete-button-icon',
'actions',
'add-button',
]);
@ -159,19 +158,20 @@ const StringControl: FC<WidgetControlProps<Pair[], KeyValueField>> = ({
variant="contained"
/>
<IconButton
icon={CloseIcon}
data-testid={`remove-button-${index}`}
size="small"
variant="text"
onClick={handleRemove(index)}
disabled={disabled}
className={classes['delete-button']}
>
<CloseIcon className={classes['delete-button-icon']} />
</IconButton>
rootClassName={classes['delete-button']}
aria-label="delete"
/>
</div>
))}
<div className={classes.actions}>
<Button
color="secondary"
variant="outlined"
onClick={handleAdd}
className={classes['add-button']}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { KeyValueField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { KeyValueField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
import type { Pair } from './types';

View File

@ -3,7 +3,7 @@
*/
import '@testing-library/jest-dom';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import { mockKeyValueField } from '@staticcms/test/data/fields.mock';
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
@ -198,7 +198,7 @@ describe(KeyValueControl.name, () => {
expect(keyInput).not.toHaveFocus();
await act(async () => {
const field = getByTestId('field');
const field = getByTestId('field-Mock Widget');
await userEvent.click(field);
});

View File

@ -5,7 +5,7 @@ import '@testing-library/jest-dom';
import converters from '../converters';
import type { KeyValueField } from '@staticcms/core/interface';
import type { KeyValueField } from '@staticcms/core';
describe('converters key value', () => {
const keyValueField: KeyValueField = {

View File

@ -6,7 +6,7 @@ import '@testing-library/jest-dom';
import validator from '../validator';
import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes';
import type { KeyValueField } from '@staticcms/core/interface';
import type { KeyValueField } from '@staticcms/core';
describe('validator key value', () => {
const t = jest.fn();

View File

@ -1,6 +1,6 @@
import { createEmptyPair } from './util';
import type { FieldStorageConverters, KeyValueField } from '@staticcms/core/interface';
import type { FieldStorageConverters, KeyValueField } from '@staticcms/core';
import type { Pair } from './types';
const converters: FieldStorageConverters<Pair[], KeyValueField, Record<string, string>> = {
@ -13,10 +13,13 @@ const converters: FieldStorageConverters<Pair[], KeyValueField, Record<string, s
: [createEmptyPair()];
},
serialize(cmsValue) {
return cmsValue?.reduce((acc, pair) => {
acc[pair.key] = pair.value;
return acc;
}, {} as Record<string, string>);
return cmsValue?.reduce(
(acc, pair) => {
acc[pair.key] = pair.value;
return acc;
},
{} as Record<string, string>,
);
},
};

View File

@ -4,7 +4,7 @@ import converters from './converters';
import schema from './schema';
import validator from './validator';
import type { KeyValueField, WidgetParam } from '@staticcms/core/interface';
import type { KeyValueField, WidgetParam } from '@staticcms/core';
import type { Pair } from './types';
const KeyValueWidget = (): WidgetParam<Pair[], KeyValueField> => {

View File

@ -2,7 +2,7 @@ import ValidationErrorTypes from '@staticcms/core/constants/validationErrorTypes
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { validateMinMax } from '@staticcms/core/lib/widgets/validations';
import type { FieldError, FieldValidationMethod, KeyValueField } from '@staticcms/core/interface';
import type { FieldError, FieldValidationMethod, KeyValueField } from '@staticcms/core';
import type { Pair } from './types';
const validator: FieldValidationMethod<Pair[], KeyValueField> = ({ field, value, t }) => {

View File

@ -6,7 +6,7 @@ import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import classNames from '@staticcms/core/lib/util/classNames.util';
import widgetListClasses from './ListControl.classes';
import type { ListField, ValueOrNestedValue, WidgetControlProps } from '@staticcms/core/interface';
import type { ListField, ValueOrNestedValue, WidgetControlProps } from '@staticcms/core';
import type { ChangeEvent, FC } from 'react';
const DelimitedListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({

View File

@ -1,24 +1,32 @@
.CMS_WidgetList_root {
&.CMS_WidgetList_disabled {
& .CMS_WidgetList_expand-button-icon {
@apply text-slate-300
dark:text-slate-600;
color: var(--text-secondary);
}
}
&:not(.CMS_WidgetList_error) {
&:not(.CMS_WidgetList_disabled) {
&:hover,
&:focus-within {
& > .CMS_WidgetList_field-wrapper {
& > .CMS_WidgetList_expand-button {
& .CMS_WidgetList_summary {
color: var(--primary-main);
}
}
}
}
}
}
&.CMS_WidgetList_error {
& .CMS_WidgetList_content {
@apply border-l-red-500;
}
}
&:not(.CMS_WidgetList_disabled) {
&:not(.CMS_WidgetList_error) {
&:hover,
&:focus-with {
& .CMS_WidgetList_summary,
& .CMS_WidgetList_expand-button-icon {
@apply text-blue-500;
&:not(.CMS_WidgetList_disabled) {
& > .CMS_WidgetList_field-wrapper {
& > .CMS_WidgetList_expand-button {
& .CMS_WidgetList_summary {
color: var(--error-main);
}
}
}
}
@ -39,13 +47,12 @@
}
.CMS_WidgetList_field-wrapper {
border-color: var(--background-divider);
@apply relative
flex
flex-col
border-b
border-slate-400
focus-within:border-blue-800
dark:focus-within:border-blue-100;
border-b;
}
.CMS_WidgetList_field {
@ -67,7 +74,6 @@
focus:outline-none
focus-visible:ring
gap-2
focus-visible:ring-opacity-75
items-center;
}
@ -78,8 +84,9 @@
}
.CMS_WidgetList_content {
@apply text-sm
text-gray-500;
color: var(--text-secondary);
@apply text-sm;
}
.CMS_WidgetList_error-message {

View File

@ -27,7 +27,7 @@ import type {
ObjectValue,
ValueOrNestedValue,
WidgetControlProps,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { FC, MouseEvent } from 'react';
import './ListControl.css';
@ -361,10 +361,12 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
{types && types.length ? (
<Menu
label={t('editor.editorWidgets.list.addType', { item: label })}
color="secondary"
variant="outlined"
buttonClassName={widgetListClasses['add-types-button']}
data-testid="list-type-add"
disabled={disabled}
aria-label="add type options dropdown"
>
<MenuGroup>
{types.map((type, idx) =>
@ -382,6 +384,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = pro
</Menu>
) : (
<Button
color="secondary"
variant="outlined"
onClick={handleAdd}
className={widgetListClasses['add-button']}

View File

@ -3,7 +3,7 @@ import React from 'react';
import { isNullish } from '@staticcms/core/lib/util/null.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core/interface';
import type { ListField, ValueOrNestedValue, WidgetPreviewProps } from '@staticcms/core';
import type { FC, ReactNode } from 'react';
const classes = generateClassNames('WidgetListPreview', ['root']);
@ -55,9 +55,7 @@ const ListPreview: FC<WidgetPreviewProps<ValueOrNestedValue[], ListField>> = ({
!['object', 'list'].includes(field.fields[0].widget)) ||
(!field.fields && !field.types) ? (
<ul style={{ marginTop: 0 }}>
{value?.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
{value?.map((item, index) => <li key={index}>{String(item)}</li>)}
</ul>
) : (
renderNestedList(value)

View File

@ -4,14 +4,14 @@
import { DndContext } from '@dnd-kit/core';
import '@testing-library/jest-dom';
import { act, getByTestId, queryByTestId, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { userEvent } from '@testing-library/user-event';
import React from 'react';
import { createWidgetControlHarness } from '@staticcms/test/harnesses/widget.harness';
import ListControl from '../ListControl';
import type { DragEndEvent } from '@dnd-kit/core';
import type { ListField, ValueOrNestedValue } from '@staticcms/core/interface';
import type { ListField, ValueOrNestedValue } from '@staticcms/core';
jest.unmock('uuid');

View File

@ -8,7 +8,7 @@ import Label from '@staticcms/core/components/common/field/Label';
import classNames from '@staticcms/core/lib/util/classNames.util';
import widgetListClasses from '../ListControl.classes';
import type { FieldError, ListField } from '@staticcms/core/interface';
import type { FieldError, ListField } from '@staticcms/core';
import type { FC, ReactNode } from 'react';
export interface ListFieldWrapperProps {
@ -44,12 +44,12 @@ const ListFieldWrapper: FC<ListFieldWrapperProps> = ({
return (
<div
data-testid="list-field"
data-testid={`list-field-${openLabel?.trim()}`}
className={classNames(
widgetListClasses.root,
disabled && widgetListClasses.disabled,
field.required !== false && widgetListClasses.required,
hasErrors && widgetListClasses.error,
(hasErrors || hasChildErrors) && widgetListClasses.error,
forSingleList && widgetListClasses['for-single-list'],
open && widgetListClasses.open,
)}
@ -68,7 +68,7 @@ const ListFieldWrapper: FC<ListFieldWrapperProps> = ({
variant="inline"
disabled={disabled}
>
{open ? openLabel : closedLabel}
{open ? openLabel.trim() : closedLabel.trim()}
</Label>
<ChevronRightIcon className={widgetListClasses['expand-button-icon']} />
</button>

View File

@ -1,9 +1,9 @@
import partial from 'lodash/partial';
import React, { useMemo } from 'react';
import { useTranslate } from 'react-polyglot';
import EditorControl from '@staticcms/core/components/entry-editor/editor-control-pane/EditorControl';
import useHasChildErrors from '@staticcms/core/lib/hooks/useHasChildErrors';
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import {
addFileTemplateFields,
@ -17,12 +17,13 @@ import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import type {
Entry,
EntryData,
Field,
ListField,
ObjectField,
ObjectValue,
ValueOrNestedValue,
WidgetControlProps,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { FC, MouseEvent } from 'react';
import type { t } from 'react-polyglot';
@ -31,6 +32,7 @@ function handleSummary(
entry: Entry,
label: string,
item: ValueOrNestedValue,
fields: Field[],
t: t,
): string {
if (typeof item === 'object' && !(item instanceof Date) && !Array.isArray(item)) {
@ -41,7 +43,7 @@ function handleSummary(
},
};
const data = addFileTemplateFields(entry.path, labeledItem);
return compileStringTemplate(summary, null, '', data);
return compileStringTemplate(summary, null, '', data, fields);
}
return isNotNullish(item) ? String(item) : t('editor.editorWidgets.list.noValue');
@ -102,7 +104,7 @@ const ListItem: FC<ListItemProps> = ({
listeners,
handleRemove,
}) => {
const t = useTranslate() as t;
const t = useTranslate();
const [summary, objectField] = useMemo((): [string, ListField | ObjectField] => {
const childObjectField: ObjectField = {
@ -138,7 +140,14 @@ const ListItem: FC<ListItemProps> = ({
const summary =
'summary' in itemType && itemType.summary ? itemType.summary : field.summary;
const labelReturn = summary
? `${label} - ${handleSummary(summary, entry, label, mixedObjectValue, t)}`
? `${label} - ${handleSummary(
summary,
entry,
label,
mixedObjectValue,
itemType.fields,
t,
)}`
: label;
return [labelReturn ?? t('editor.editorWidgets.list.noValue'), itemType];
@ -165,7 +174,7 @@ const ListItem: FC<ListItemProps> = ({
const summary = field.summary;
const labelReturn = summary
? handleSummary(summary, entry, String(labelFieldValue), objectValue, t)
? handleSummary(summary, entry, String(labelFieldValue), objectValue, multiFields, t)
: labelFieldValue
? String(labelFieldValue)
: undefined;

View File

@ -4,8 +4,14 @@
flex-col;
&.CMS_WidgetList_ListItem_error {
& .CMS_WidgetList_ListItem_content {
@apply border-l-red-500;
& > .MuiCollapse-root {
& > .MuiCollapse-wrapper {
& > .MuiCollapse-wrapperInner {
& > .CMS_WidgetList_ListItem_content {
border-color: var(--error-main);
}
}
}
}
}
@ -14,18 +20,24 @@
&:hover {
& .CMS_WidgetList_ListItem_summary-label,
& .CMS_WidgetList_ListItem_expand-button-icon {
@apply text-blue-500;
color: var(--primary-main);
}
}
&:focus-within {
& .CMS_WidgetList_ListItem_summary-label,
& .CMS_WidgetList_ListItem_expand-button-icon {
@apply text-blue-500;
color: var(--primary-main);
}
& .CMS_WidgetList_ListItem_content {
@apply border-l-blue-500;
& > .MuiCollapse-root {
& > .MuiCollapse-wrapper {
& > .MuiCollapse-wrapperInner {
& > .CMS_WidgetList_ListItem_content {
border-color: var(--primary-main);
}
}
}
}
}
}
@ -33,8 +45,7 @@
&.CMS_WidgetList_ListItem_disabled {
& .CMS_WidgetList_ListItem_expand-button-icon {
@apply text-slate-300
dark:text-slate-600;
color: var(--text-secondary);
}
}
@ -52,16 +63,16 @@
flex-col;
&.CMS_WidgetList_ListItem_error {
& .CMS_WidgetList_ListItem_content {
@apply border-l-red-500;
& > .CMS_WidgetList_ListItem_content {
border-color: var(--error-main);
}
}
&:not(.CMS_WidgetList_ListItem_error) {
&:not(.CMS_WidgetList_ListItem_disabled) {
&:focus-within {
& .CMS_WidgetList_ListItem_content {
@apply border-l-blue-500;
& > .CMS_WidgetList_ListItem_content {
border-color: var(--primary-main);
}
}
}
@ -89,7 +100,6 @@
focus:outline-none
focus-visible:ring
gap-2
focus-visible:ring-opacity-75
items-center;
}
@ -118,30 +128,33 @@
}
.CMS_WidgetList_ListItem_not-open-placeholder {
border-color: var(--background-light);
@apply ml-8
border-b
border-slate-400;
border-b;
}
.CMS_WidgetList_ListItem_content {
color: var(--text-secondary);
border-color: var(--background-light);
@apply relative
ml-4
text-sm
text-gray-500
border-l-2
border-solid
border-l-slate-400;
flex
gap-2;
}
.CMS_WidgetList_ListItem_content-fields {
@apply relative
flex-grow;
}
.CMS_WidgetList_ListItem_single-field-controls {
@apply absolute
right-3
top-0
h-full
flex
items-center
justify-end
z-10;
@apply flex
flex-shrink-0;
}
.CMS_WidgetList_ListItem_drag-handle {

View File

@ -10,7 +10,7 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
import type { MouseEvent, ReactNode } from 'react';
import type { FC, MouseEvent, ReactNode } from 'react';
import './ListItemWrapper.css';
@ -30,6 +30,7 @@ const classes = generateClassNames('WidgetList_ListItem', [
'button-icon',
'not-open-placeholder',
'content',
'content-fields',
'single-field-controls',
'drag-handle',
'drag-handle-icon',
@ -40,7 +41,7 @@ export interface DragHandleProps {
disabled: boolean;
}
const DragHandle = ({ listeners, disabled }: DragHandleProps) => {
const DragHandle: FC<DragHandleProps> = ({ listeners, disabled }) => {
return (
<span
data-testid="drag-handle"
@ -65,7 +66,7 @@ export interface ListItemWrapperProps {
disabled: boolean;
}
const ListItemWrapper = ({
const ListItemWrapper: FC<ListItemWrapperProps> = ({
label,
summary,
collapsed = false,
@ -75,7 +76,7 @@ const ListItemWrapper = ({
children,
isSingleField,
disabled,
}: ListItemWrapperProps) => {
}) => {
const [open, setOpen] = useState(!collapsed);
const handleOpenToggle = useCallback(() => {
@ -87,15 +88,17 @@ const ListItemWrapper = ({
<div className={classes.controls}>
{onRemove ? (
<IconButton
icon={CloseIcon}
data-testid="remove-button"
size="small"
color="secondary"
variant="text"
onClick={onRemove}
disabled={disabled}
className={classes['remove-button']}
>
<CloseIcon className={classes['button-icon']} />
</IconButton>
rootClassName={classes['remove-button']}
iconClassName={classes['button-icon']}
aria-label="remove"
/>
) : null}
{listeners ? <DragHandle listeners={listeners} disabled={disabled} /> : null}
</div>
@ -106,7 +109,7 @@ const ListItemWrapper = ({
if (isSingleField) {
return (
<div
data-testid="list-item-field"
data-testid={`list-item-field-${label?.trim()}`}
className={classNames(
classes['single-field-root'],
hasErrors && classes.error,
@ -114,7 +117,7 @@ const ListItemWrapper = ({
)}
>
<div data-testid="list-item-objects" className={classes.content}>
{children}
<div className={classes['content-fields']}>{children}</div>
<div className={classes['single-field-controls']}>{renderedControls}</div>
</div>
</div>
@ -123,7 +126,7 @@ const ListItemWrapper = ({
return (
<div
data-testid="list-item-field"
data-testid={`list-item-field-${label?.trim()}`}
className={classNames(
classes.root,
hasErrors && classes.error,
@ -136,6 +139,7 @@ const ListItemWrapper = ({
data-testid="list-item-expand-button"
className={classes['expand-button']}
onClick={handleOpenToggle}
aria-label={!open ? 'expand' : 'collapse'}
>
<ChevronRightIcon className={classes['expand-button-icon']} />
<div className={classes.summary}>
@ -148,7 +152,7 @@ const ListItemWrapper = ({
data-testid="item-label"
disabled={disabled}
>
{label}
{label.trim()}
</Label>
{!open ? <span data-testid="item-summary">{summary}</span> : null}
</div>
@ -157,7 +161,9 @@ const ListItemWrapper = ({
</div>
{!open ? <div className={classes['not-open-placeholder']}></div> : null}
<Collapse in={open} appear={false}>
<div className={classes.content}>{children}</div>
<div className={classes.content}>
<div className={classes['content-fields']}>{children}</div>
</div>
</Collapse>
</div>
);

View File

@ -2,7 +2,7 @@ import controlComponent from './ListControl';
import previewComponent from './ListPreview';
import schema from './schema';
import type { ListField, ValueOrNestedValue, WidgetParam } from '@staticcms/core/interface';
import type { ListField, ValueOrNestedValue, WidgetParam } from '@staticcms/core';
const ListWidget = (): WidgetParam<ValueOrNestedValue[], ListField> => {
return {

View File

@ -1,4 +1,4 @@
import type { ListField, ObjectField, ObjectValue } from '@staticcms/core/interface';
import type { ListField, ObjectField, ObjectValue } from '@staticcms/core';
export const TYPES_KEY = 'types';
export const TYPE_KEY = 'type_key';

View File

@ -2,7 +2,7 @@ import React from 'react';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { MapField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { MapField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
const classes = generateClassNames('WidgetMapPreview', ['root']);

View File

@ -2,7 +2,7 @@ import previewComponent from './MapPreview';
import schema from './schema';
import withMapControl from './withMapControl';
import type { MapField, WidgetParam } from '@staticcms/core/interface';
import type { MapField, WidgetParam } from '@staticcms/core';
const controlComponent = withMapControl();

View File

@ -12,7 +12,7 @@ import Field from '@staticcms/core/components/common/field/Field';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import type { MapField, WidgetControlProps } from '@staticcms/core/interface';
import type { MapField, WidgetControlProps } from '@staticcms/core';
import type { Geometry } from 'ol/geom';
import type { FC } from 'react';

View File

@ -10,7 +10,7 @@ import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
import useMdx from './plate/hooks/useMdx';
import { processShortcodeConfigToMdx } from './plate/serialization/slate/processShortcodeConfig';
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
import type { UseMdxState } from './plate/hooks/useMdx';

View File

@ -2,7 +2,7 @@ import withMarkdownControl from './withMarkdownControl';
import previewComponent from './MarkdownPreview';
import schema from './schema';
import type { MarkdownField, WidgetParam } from '@staticcms/core/interface';
import type { MarkdownField, WidgetParam } from '@staticcms/core';
const controlComponent = withMarkdownControl({ useMdx: false });

View File

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { getShortcode } from '../../../lib/registry';
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core';
import type { FC } from 'react';
export interface WithShortcodeMdxComponentProps {

View File

@ -54,13 +54,14 @@ import {
import React, { useMemo, useRef } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslate } from 'react-polyglot';
import useTranslate from '@staticcms/core/lib/hooks/useTranslate';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import widgetMarkdownClasses from '../MarkdownControl.classes';
import { CodeBlockElement, withShortcodeElement } from './components';
import { BalloonToolbar } from './components/balloon-toolbar';
import { BlockquoteElement } from './components/nodes/blockquote';
import CodeElement from './components/nodes/code/Code';
import {
Heading1,
Heading2,
@ -84,7 +85,6 @@ import {
TableHeaderCellElement,
TableRowElement,
} from './components/nodes/table';
import CodeElement from './components/nodes/code/Code';
import { Toolbar } from './components/toolbar';
import editableProps from './editableProps';
import { createMdPlugins, ELEMENT_SHORTCODE } from './plateTypes';
@ -101,19 +101,18 @@ import { createTablePlugin } from './plugins/table';
import { trailingBlockPlugin } from './plugins/trailing-block';
import type {
Collection,
CollectionWithDefaults,
Entry,
MarkdownField,
WidgetControlProps,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { AnyObject, AutoformatPlugin, PlatePlugin } from '@udecode/plate';
import type { FC } from 'react';
import type { t as T } from 'react-polyglot';
import type { MdEditor, MdValue } from './plateTypes';
export interface PlateEditorProps {
initialValue: MdValue;
collection: Collection<MarkdownField>;
collection: CollectionWithDefaults<MarkdownField>;
entry: Entry;
field: MarkdownField;
useMdx: boolean;
@ -134,7 +133,7 @@ const PlateEditor: FC<PlateEditorProps> = ({
onFocus,
onBlur,
}) => {
const t = useTranslate() as T;
const t = useTranslate();
const editorContainerRef = useRef<HTMLDivElement | null>(null);
const innerEditorContainerRef = useRef<HTMLDivElement | null>(null);

View File

@ -3,22 +3,18 @@
}
.CMS_WidgetMarkdown_BalloonToolbar_popper {
background: var(--background-main);
@apply absolute
max-h-60
overflow-hidden
rounded-md
bg-white
p-1
text-base
shadow-md
ring-1
ring-black
ring-opacity-5
focus:outline-none
sm:text-sm
z-[100]
dark:bg-slate-700
dark:shadow-lg;
z-[100];
}
.CMS_WidgetMarkdown_BalloonToolbar_content {

View File

@ -38,12 +38,12 @@ import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import { getToolbarButtons } from '../../hooks/useToolbarButtons';
import type {
Collection,
CollectionWithDefaults,
MarkdownField,
MarkdownToolbarButtonType,
} from '@staticcms/core/interface';
import type { FC, ReactNode } from 'react';
} from '@staticcms/core';
import type { ClientRectObject } from '@udecode/plate';
import type { FC, ReactNode } from 'react';
import './BalloonToolbar.css';
@ -91,7 +91,7 @@ const DEFAULT_TABLE_SELECTION_BUTTONS: MarkdownToolbarButtonType[] = [
export interface BalloonToolbarProps {
useMdx: boolean;
containerRef: HTMLElement | null;
collection: Collection<MarkdownField>;
collection: CollectionWithDefaults<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}

View File

@ -20,15 +20,15 @@ import {
import React, { useRef } from 'react';
import { useFocused } from 'slate-react';
import { configLoaded } from '@staticcms/core/actions/config';
import { applyDefaults, configLoaded } from '@staticcms/core/actions/config';
import { store } from '@staticcms/core/store';
import { createMockCollection } from '@staticcms/test/data/collections.mock';
import { createMockConfig } from '@staticcms/test/data/config.mock';
import { createMockFolderCollectionWithDefaults } from '@staticcms/test/data/collections.mock';
import { createNoDefaultsMockConfig } from '@staticcms/test/data/config.mock';
import { mockMarkdownField } from '@staticcms/test/data/fields.mock';
import { renderWithProviders } from '@staticcms/test/test-utils';
import BalloonToolbar from '../BalloonToolbar';
import type { Config, MarkdownField } from '@staticcms/core/interface';
import type { Config, ConfigWithDefaults } from '@staticcms/core';
import type { MdEditor } from '@staticcms/markdown/plate/plateTypes';
import type { TRange } from '@udecode/plate';
import type { FC } from 'react';
@ -48,7 +48,7 @@ const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false
key="balloon-toolbar"
useMdx={useMdx}
containerRef={ref.current}
collection={createMockCollection({}, mockMarkdownField)}
collection={createMockFolderCollectionWithDefaults({}, mockMarkdownField)}
field={mockMarkdownField}
disabled={false}
/>
@ -56,9 +56,11 @@ const BalloonToolbarWrapper: FC<BalloonToolbarWrapperProps> = ({ useMdx = false
);
};
const config = createMockConfig({
const originalConfig = createNoDefaultsMockConfig({
collections: [],
}) as unknown as Config<MarkdownField>;
}) as unknown as Config;
let config: ConfigWithDefaults;
describe(BalloonToolbar.name, () => {
const mockUseEditor = usePlateEditorState as jest.Mock;
@ -79,7 +81,9 @@ describe(BalloonToolbar.name, () => {
beforeEach(() => {
jest.useFakeTimers();
store.dispatch(configLoaded(config as unknown as Config));
config = applyDefaults(originalConfig) as unknown as ConfigWithDefaults;
store.dispatch(configLoaded(config, originalConfig));
mockEditor = {
selection: undefined,

View File

@ -3,6 +3,7 @@ import { ELEMENT_BLOCKQUOTE } from '@udecode/plate';
import React from 'react';
import BlockToolbarButton from './common/BlockToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,10 +13,13 @@ export interface BlockquoteToolbarButtonProps {
}
const BlockquoteToolbarButton: FC<BlockquoteToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<BlockToolbarButton
label="Blockquote"
tooltip="Insert blockquote"
id="blockquote"
label={t('editor.editorWidgets.markdown.quote')}
tooltip={t('editor.editorWidgets.markdown.insertQuote')}
icon={FormatQuoteIcon}
type={ELEMENT_BLOCKQUOTE}
disabled={disabled}

View File

@ -3,6 +3,7 @@ import { MARK_BOLD } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface BoldToolbarButtonProps {
}
const BoldToolbarButton: FC<BoldToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<MarkToolbarButton
tooltip="Bold"
id="bold"
tooltip={t('editor.editorWidgets.markdown.bold')}
type={MARK_BOLD}
variant={variant}
icon={FormatBoldIcon}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface CodeBlockToolbarButtonsProps {
}
const CodeBlockToolbarButtons: FC<CodeBlockToolbarButtonsProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleCodeBlockOnClick = useCallback(() => {
@ -23,8 +26,9 @@ const CodeBlockToolbarButtons: FC<CodeBlockToolbarButtonsProps> = ({ disabled, v
return (
<ToolbarButton
label="Code block"
tooltip="Insert code block"
id="code-block"
label={t('editor.editorWidgets.markdown.codeBlock')}
tooltip={t('editor.editorWidgets.markdown.insertCodeBlock')}
icon={CodeIcon}
onClick={handleCodeBlockOnClick}
disabled={disabled}

View File

@ -3,6 +3,7 @@ import { MARK_CODE } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface CodeToolbarButtonProps {
}
const CodeToolbarButton: FC<CodeToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<MarkToolbarButton
tooltip="Code"
id="code"
tooltip={t('editor.editorWidgets.markdown.code')}
type={MARK_CODE}
icon={CodeIcon}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -16,6 +17,8 @@ const DecreaseIndentToolbarButton: FC<DecreaseIndentToolbarButtonProps> = ({
disabled,
variant,
}) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleOutdent = useCallback(() => {
@ -24,7 +27,8 @@ const DecreaseIndentToolbarButton: FC<DecreaseIndentToolbarButtonProps> = ({
return (
<ToolbarButton
tooltip="Decrease indent"
id="decrease-ident"
tooltip={t('editor.editorWidgets.markdown.decreaseIndent')}
onClick={handleOutdent}
icon={FormatIndentDecreaseIcon}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface DeleteColumnToolbarButtonProps {
}
const DeleteColumnToolbarButton: FC<DeleteColumnToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleDeleteColumn = useCallback(() => {
@ -21,7 +24,8 @@ const DeleteColumnToolbarButton: FC<DeleteColumnToolbarButtonProps> = ({ disable
return (
<ToolbarButton
tooltip="Delete column"
id="delete-column"
tooltip={t('editor.editorWidgets.markdown.table.deleteColumn')}
icon={TableDeleteColumn}
onClick={handleDeleteColumn}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface DeleteRowToolbarButtonProps {
}
const DeleteRowToolbarButton: FC<DeleteRowToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleDeleteRow = useCallback(() => {
@ -21,7 +24,8 @@ const DeleteRowToolbarButton: FC<DeleteRowToolbarButtonProps> = ({ disabled, var
return (
<ToolbarButton
tooltip="Delete row"
id="delete-row"
tooltip={t('editor.editorWidgets.markdown.table.deleteRow')}
icon={TableDeleteRow}
onClick={handleDeleteRow}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface DeleteTableToolbarButtonProps {
}
const DeleteTableToolbarButton: FC<DeleteTableToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleDeleteTable = useCallback(() => {
@ -21,7 +24,8 @@ const DeleteTableToolbarButton: FC<DeleteTableToolbarButtonProps> = ({ disabled,
return (
<ToolbarButton
tooltip="Delete table"
id="delete-table"
tooltip={t('editor.editorWidgets.markdown.table.deleteTable')}
icon={TableDismiss}
onClick={handleDeleteTable}
disabled={disabled}

View File

@ -5,15 +5,16 @@
&.CMS_WidgetMarkdown_FontTypeSelect_disabled {
& .CMS_WidgetMarkdown_FontTypeSelect_select {
@apply text-gray-300
border-gray-200
dark:border-gray-600
dark:text-gray-500;
color: var(--text-secondary);
border-color: var(--background-light);
}
}
}
.CMS_WidgetMarkdown_FontTypeSelect_select {
color: var(--text-primary);
border-color: var(--background-light);
@apply flex
items-center
justify-between
@ -25,42 +26,35 @@
w-full
h-6
border
rounded-sm
text-gray-800
border-gray-600
dark:border-gray-400
dark:text-gray-100;
rounded-sm;
}
.CMS_WidgetMarkdown_FontTypeSelect_popper {
background: var(--background-main);
@apply max-h-40
overflow-auto
rounded-md
bg-white
shadow-md
ring-1
ring-black
ring-opacity-5
focus:outline-none
sm:text-sm
dark:bg-slate-800
dark:shadow-lg
z-[101];
}
.CMS_WidgetMarkdown_FontTypeSelect_option {
&:hover {
color: var(--primary-contrast-color);
background: var(--primary-main);
}
@apply relative
select-none
py-2
px-4
cursor-pointer
hover:bg-blue-500
hover:text-white
dark:hover:bg-blue-500;
cursor-pointer;
&.CMS_WidgetMarkdown_FontTypeSelect_option-selected {
@apply bg-blue-500/25
dark:bg-blue-700/20;
background: var(--primary-main);
& .CMS_WidgetMarkdown_FontTypeSelect_option-label {
@apply font-medium;

View File

@ -18,6 +18,7 @@ import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import classNames from '@staticcms/core/lib/util/classNames.util';
import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import { useTranslate } from '@staticcms/core/lib';
import type { SelectRootSlotProps } from '@mui/base/Select';
import type { FC, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
@ -44,31 +45,31 @@ type Option = {
const types: Option[] = [
{
value: ELEMENT_H1,
label: 'Heading 1',
label: 'editor.editorWidgets.headingOptions.headingOne',
},
{
value: ELEMENT_H2,
label: 'Heading 2',
label: 'editor.editorWidgets.headingOptions.headingTwo',
},
{
value: ELEMENT_H3,
label: 'Heading 3',
label: 'editor.editorWidgets.headingOptions.headingThree',
},
{
value: ELEMENT_H4,
label: 'Heading 4',
label: 'editor.editorWidgets.headingOptions.headingFour',
},
{
value: ELEMENT_H5,
label: 'Heading 5',
label: 'editor.editorWidgets.headingOptions.headingFive',
},
{
value: ELEMENT_H6,
label: 'Heading 6',
label: 'editor.editorWidgets.headingOptions.headingSix',
},
{
value: ELEMENT_PARAGRAPH,
label: 'Paragraph',
label: 'editor.editorWidgets.markdown.paragraph',
},
];
@ -93,6 +94,8 @@ export interface FontTypeSelectProps {
* Toolbar button to toggle the type of elements in selection.
*/
const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const [version, setVersion] = useState(0);
@ -151,7 +154,7 @@ const FontTypeSelect: FC<FontTypeSelectProps> = ({ disabled = false }) => {
},
}}
>
<span className={classes['option-label']}>{type.label}</span>
<span className={classes['option-label']}>{t(type.label)}</span>
</Option>
);
})}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -16,6 +17,8 @@ const IncreaseIndentToolbarButton: FC<IncreaseIndentToolbarButtonProps> = ({
disabled,
variant,
}) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleIndent = useCallback(() => {
@ -24,7 +27,8 @@ const IncreaseIndentToolbarButton: FC<IncreaseIndentToolbarButtonProps> = ({
return (
<ToolbarButton
tooltip="Increase indent"
id="increase-ident"
tooltip={t('editor.editorWidgets.markdown.increaseIndent')}
onClick={handleIndent}
icon={FormatIndentIncreaseIcon}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface InsertColumnToolbarButtonProps {
}
const InsertColumnToolbarButton: FC<InsertColumnToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleInsertTableColumn = useCallback(() => {
@ -21,7 +24,8 @@ const InsertColumnToolbarButton: FC<InsertColumnToolbarButtonProps> = ({ disable
return (
<ToolbarButton
tooltip="Insert column"
id="insert-column"
tooltip={t('editor.editorWidgets.markdown.table.insertColumn')}
icon={TableInsertColumn}
onClick={handleInsertTableColumn}
disabled={disabled}

View File

@ -2,12 +2,13 @@ import { Image as ImageIcon } from '@styled-icons/material/Image';
import { ELEMENT_IMAGE, getAboveNode, setNodes } from '@udecode/plate';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslate } from '@staticcms/core/lib';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
import type { CollectionWithDefaults, MarkdownField, MediaPath } from '@staticcms/core';
import type { MdImageElement } from '@staticcms/markdown/plate/plateTypes';
import type { FC } from 'react';
import type { BaseSelection } from 'slate';
@ -15,7 +16,7 @@ import type { BaseSelection } from 'slate';
export interface InsertImageToolbarButtonProps {
variant: 'button' | 'menu';
currentValue?: { url: string; alt?: string };
collection: Collection<MarkdownField>;
collection: CollectionWithDefaults<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}
@ -27,6 +28,8 @@ const InsertImageToolbarButton: FC<InsertImageToolbarButtonProps> = ({
currentValue,
disabled,
}) => {
const t = useTranslate();
const [selection, setSelection] = useState<BaseSelection>();
const editor = useMdPlateEditorState();
const handleInsert = useCallback(
@ -83,8 +86,9 @@ const InsertImageToolbarButton: FC<InsertImageToolbarButtonProps> = ({
return (
<ToolbarButton
label="Image"
tooltip="Insert image"
id="image"
label={t('editor.editorWidgets.markdown.image')}
tooltip={t('editor.editorWidgets.markdown.insertImage')}
icon={ImageIcon}
onClick={handleOpenMediaLibrary}
disabled={disabled}

View File

@ -12,13 +12,14 @@ import {
} from '@udecode/plate';
import React, { useCallback, useMemo } from 'react';
import { useTranslate } from '@staticcms/core/lib';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
import { useMdPlateEditorState } from '@staticcms/markdown/plate/plateTypes';
import ToolbarButton from './common/ToolbarButton';
import type { Collection, MarkdownField, MediaPath } from '@staticcms/core/interface';
import type { CollectionWithDefaults, MarkdownField, MediaPath } from '@staticcms/core';
import type { MdLinkElement } from '@staticcms/markdown/plate/plateTypes';
import type { TText } from '@udecode/plate';
import type { FC } from 'react';
@ -27,7 +28,7 @@ import type { Location } from 'slate';
export interface InsertLinkToolbarButtonProps {
variant: 'button' | 'menu';
currentValue?: { url: string; alt?: string };
collection: Collection<MarkdownField>;
collection: CollectionWithDefaults<MarkdownField>;
field: MarkdownField;
disabled: boolean;
}
@ -39,6 +40,8 @@ const InsertLinkToolbarButton: FC<InsertLinkToolbarButtonProps> = ({
currentValue,
disabled,
}) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleInsert = useCallback(
({ path: newUrl, alt: newText }: MediaPath<string>) => {
@ -107,8 +110,9 @@ const InsertLinkToolbarButton: FC<InsertLinkToolbarButtonProps> = ({
return !isLink ? (
<ToolbarButton
label="Link"
tooltip="Insert link"
id="link"
label={t('editor.editorWidgets.markdown.link')}
tooltip={t('editor.editorWidgets.markdown.insertLink')}
icon={LinkIcon}
onClick={handleOpenMediaLibrary}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -13,6 +14,8 @@ export interface InsertRowToolbarButtonProps {
}
const InsertRowToolbarButton: FC<InsertRowToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleInsertTableRow = useCallback(() => {
@ -21,7 +24,8 @@ const InsertRowToolbarButton: FC<InsertRowToolbarButtonProps> = ({ disabled, var
return (
<ToolbarButton
tooltip="Insert row"
id="insert-row"
tooltip={t('editor.editorWidgets.markdown.table.insertRow')}
icon={TableInsertRow}
onClick={handleInsertTableRow}
disabled={disabled}

View File

@ -4,6 +4,7 @@ import React, { useCallback } from 'react';
import { useMdPlateEditorState } from '../../plateTypes';
import ToolbarButton from './common/ToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -16,6 +17,8 @@ const InsertTableToolbarButton: FC<InsertTableToolbarButtonProps> = ({
disabled,
variant = 'button',
}) => {
const t = useTranslate();
const editor = useMdPlateEditorState();
const handleTableAdd = useCallback(() => {
@ -27,8 +30,9 @@ const InsertTableToolbarButton: FC<InsertTableToolbarButtonProps> = ({
return (
<ToolbarButton
label="Table"
tooltip="Insert table"
id="insert-table"
label={t('editor.editorWidgets.markdown.table.table')}
tooltip={t('editor.editorWidgets.markdown.table.insertTable')}
icon={TableAdd}
onClick={handleTableAdd}
disabled={disabled}

View File

@ -3,6 +3,7 @@ import { MARK_ITALIC } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface ItalicToolbarButtonsProp {
}
const ItalicToolbarButton: FC<ItalicToolbarButtonsProp> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<MarkToolbarButton
tooltip="Italic"
id="italic"
tooltip={t('editor.editorWidgets.markdown.italic')}
type={MARK_ITALIC}
variant={variant}
icon={FormatItalicIcon}

View File

@ -3,6 +3,7 @@ import { ELEMENT_OL } from '@udecode/plate';
import React from 'react';
import ListToolbarButton from './common/ListToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface OrderedListToolbarButtonProps {
}
const OrderedListToolbarButton: FC<OrderedListToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<ListToolbarButton
tooltip="Numbered list"
id="numbered-list"
tooltip={t('editor.editorWidgets.markdown.numberedList')}
type={ELEMENT_OL}
icon={FormatListNumberedIcon}
disabled={disabled}

View File

@ -48,10 +48,12 @@ const ShortcodeToolbarButton: FC<ShortcodeToolbarButtonProps> = ({ disabled }) =
data-testid="toolbar-button-shortcode"
keepMounted
hideDropdownIcon
color="secondary"
variant="text"
rootClassName={classes.root}
buttonClassName={classes.button}
disabled={disabled}
aria-label="add shortcode"
>
<MenuGroup>
{Object.keys(configs).map(name => {

View File

@ -3,6 +3,7 @@ import { MARK_STRIKETHROUGH } from '@udecode/plate';
import React from 'react';
import MarkToolbarButton from './common/MarkToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface StrikethroughToolbarButtonProps {
}
const StrikethroughToolbarButton: FC<StrikethroughToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<MarkToolbarButton
tooltip="Strikethrough"
id="strikethrough"
tooltip={t('editor.editorWidgets.markdown.strikethrough')}
type={MARK_STRIKETHROUGH}
variant={variant}
icon={FormatStrikethroughIcon}

View File

@ -3,6 +3,7 @@ import { ELEMENT_UL } from '@udecode/plate';
import React from 'react';
import ListToolbarButton from './common/ListToolbarButton';
import { useTranslate } from '@staticcms/core/lib';
import type { FC } from 'react';
@ -12,9 +13,12 @@ export interface UnorderedListToolbarButtonProps {
}
const UnorderedListToolbarButton: FC<UnorderedListToolbarButtonProps> = ({ disabled, variant }) => {
const t = useTranslate();
return (
<ListToolbarButton
tooltip="List"
id="bulleted-list"
tooltip={t('editor.editorWidgets.markdown.bulletedList')}
type={ELEMENT_UL}
icon={FormatListBulletedIcon}
disabled={disabled}

View File

@ -5,10 +5,8 @@
&.CMS_WidgetMarkdown_ToolbarButton_active {
&:not(.CMS_WidgetMarkdown_ToolbarButton_custom-active-color) {
@apply text-blue-500
bg-gray-100
dark:text-blue-500
dark:bg-slate-800;
color: var(--primary-main);
background: color-mix(in srgb, var(--primary-main) 15%, transparent);
}
}
}

View File

@ -19,6 +19,7 @@ const classes = generateClassNames('WidgetMarkdown_ToolbarButton', [
]);
export interface ToolbarButtonProps {
id: string;
label?: string;
tooltip: string;
active?: boolean;
@ -31,6 +32,7 @@ export interface ToolbarButtonProps {
}
const ToolbarButton: FC<ToolbarButtonProps> = ({
id,
icon: Icon,
tooltip,
label,
@ -79,9 +81,10 @@ const ToolbarButton: FC<ToolbarButtonProps> = ({
<Button
key="button"
aria-label={label ?? tooltip}
color="secondary"
title={label ?? tooltip}
variant="text"
data-testid={`toolbar-button-${label ?? tooltip}`.replace(' ', '-').toLowerCase()}
data-testid={`toolbar-button-${id}`}
onClick={handleOnClick}
className={classNames(
classes.root,

View File

@ -1,11 +1,13 @@
.CMS_WidgetMarkdown_ColorButton_root {
&.CMS_WidgetMarkdown_ColorButton_is-bright-color {
& .CMS_WidgetMarkdown_ColorButton_avatar {
border: 1px solid rgba(209, 213, 219, 1);
border-color: var(--background-light);
@apply border;
}
& .CMS_WidgetMarkdown_ColorButton_check-icon {
@apply text-black;
color: var(--text-primary);
}
}
}
@ -17,7 +19,8 @@
}
.CMS_WidgetMarkdown_ColorButton_check-icon {
color: var(--primary-contrast-color);
@apply h-5
w-5
text-white;
w-5;
}

View File

@ -41,6 +41,7 @@ const ColorButton: FC<ColorButtonProps> = ({
onClick={handleOnClick}
sx={{ p: 0 }}
className={classNames(classes.root, isBrightColor && classes['is-bright-color'])}
aria-label={value}
>
<Avatar
alt={name}

View File

@ -1,20 +1,16 @@
.CMS_WidgetMarkdown_MediaPopover_root {
background: var(--background-main);
@apply absolute
max-h-60
overflow-auto
rounded-md
bg-white
p-1
text-base
shadow-md
ring-1
ring-black
ring-opacity-5
focus:outline-none
sm:text-sm
z-40
dark:bg-slate-700
dark:shadow-lg;
z-40;
}
.CMS_WidgetMarkdown_MediaPopover_content {
@ -28,8 +24,8 @@
}
.CMS_WidgetMarkdown_MediaPopover_divider {
border-color: var(--background-light);
@apply w-[1px]
border
border-gray-100
dark:border-slate-600;
border;
}

View File

@ -9,11 +9,11 @@ import { generateClassNames } from '@staticcms/core/lib/util/theming.util';
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
import type {
Collection,
CollectionWithDefaults,
FileOrImageField,
MarkdownField,
MediaPath,
} from '@staticcms/core/interface';
} from '@staticcms/core';
import type { MouseEvent } from 'react';
import './MediaPopover.css';
@ -30,7 +30,7 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
url: string;
text?: string;
forImage?: boolean;
collection: Collection<T>;
collection: CollectionWithDefaults<T>;
field: T;
onMediaToggle?: (open: boolean) => void;
onMediaChange: (newValue: MediaPath<string>) => void;
@ -101,16 +101,16 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
className={classes.root}
>
<div key="edit-content" contentEditable={false} className={classes.content}>
<Button onClick={handleOpenMediaLibrary} variant="text" size="small">
<Button onClick={handleOpenMediaLibrary} color="secondary" variant="text" size="small">
{forImage ? 'Edit Image' : 'Edit Link'}
</Button>
<div className={classes.divider} />
{!forImage ? (
<Button href={url} variant="text" size="small" onClick={noop}>
<Button href={url} color="secondary" variant="text" size="small" onClick={noop}>
<OpenInNewIcon className={classes.icon} title="Open In New Tab" />
</Button>
) : null}
<Button onClick={onRemove} variant="text" size="small">
<Button onClick={onRemove} color="secondary" variant="text" size="small">
<DeleteForeverIcon className={classes.icon} title="Delete" />
</Button>
</div>

Some files were not shown because too many files have changed in this diff Show More