Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/volto/cypress/tests/core/basic/recurrence-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ const createEvent = () => {
cy.get('#toolbar-add-event').click();
cy.get('#field-title').type('Test recurrence');

cy.get('input#start-date').type('{selectall}05/04/2020{esc}'); //May,4 2020
cy.get('input#start-time').type('{selectall}01:00 AM{esc}');
cy.get('input#end-date').type('{selectall}05/16/2020{esc}'); //May,16 2020
cy.get('input#end-time').type('{selectall}01:00 AM{esc}');
// Set start date: May 4, 2020
cy.get('.field-wrapper-start [data-type="month"]').click().type('5');
cy.get('.field-wrapper-start [data-type="day"]').click().type('4');
cy.get('.field-wrapper-start [data-type="year"]').click().type('2020');
cy.get('.field-wrapper-start [data-type="hour"]').click().type('1');
cy.get('.field-wrapper-start [data-type="minute"]').click().type('00');
// Set end date: May 16, 2020
cy.get('.field-wrapper-end [data-type="month"]').click().type('5');
cy.get('.field-wrapper-end [data-type="day"]').click().type('16');
cy.get('.field-wrapper-end [data-type="year"]').click().type('2020');
cy.get('.field-wrapper-end [data-type="hour"]').click().type('1');
cy.get('.field-wrapper-end [data-type="minute"]').click().type('00');
};

const openRecurrenceModal = () => {
Expand Down
37 changes: 18 additions & 19 deletions packages/volto/cypress/tests/core/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ describe('Add Content Tests', () => {
// because of lazyloading wait for the element to reach an actionable state
.clear()
.type('datetimeWidget test');
cy.get('#start-time').click();
cy.get('.rc-time-picker-panel-input').click();
cy.get('.rc-time-picker-panel-input').clear().type('6:40 AM');
// Set time using react-aria date segments
cy.get('.field-wrapper-start [data-type="hour"]').click().type('6');
cy.get('.field-wrapper-start [data-type="minute"]').click().type('40');
cy.get('#toolbar-save').click();

// then
Expand All @@ -170,38 +170,37 @@ describe('Add Content Tests', () => {
cy.get('#toolbar-add-event').click();
cy.get('#field-title').clear().type('Event checkbox test');

// then the time fields should be visible initially
cy.get('#start-time').should('be.visible');
cy.get('#end-time').should('be.visible');
cy.get('#end-date').should('be.visible');
// then the time segments should be visible initially
cy.get('.field-wrapper-start [data-type="hour"]').should('be.visible');
cy.get('.field-wrapper-end [data-type="hour"]').should('be.visible');
cy.get('.field-wrapper-end').should('be.visible');

// when I check the whole_day checkbox
cy.get('label[for="field-whole_day"]').click({ scrollBehavior: false });

// then the time fields should disappear
cy.get('#start-time').should('not.exist');
cy.get('#end-time').should('not.exist');
// then the time segments should disappear (granularity switches to day)
cy.get('.field-wrapper-start [data-type="hour"]').should('not.exist');
cy.get('.field-wrapper-end [data-type="hour"]').should('not.exist');

// when I uncheck the whole_day checkbox
cy.get('label[for="field-whole_day"]').click({ scrollBehavior: false });

// then the time fields should be visible again
cy.get('#start-time').should('be.visible');
cy.get('#end-time').should('be.visible');
// then the time segments should be visible again
cy.get('.field-wrapper-start [data-type="hour"]').should('be.visible');
cy.get('.field-wrapper-end [data-type="hour"]').should('be.visible');

// when I check the open_end checkbox
cy.get('label[for="field-open_end"]').click({ scrollBehavior: false });

// then the end-date and end-time fields should disappear
cy.get('#end-date').should('not.exist');
cy.get('#end-time').should('not.exist');
// then the end field should disappear entirely
cy.get('.field-wrapper-end').should('not.exist');

// when I uncheck the open_end checkbox
cy.get('label[for="field-open_end"]').click({ scrollBehavior: false });

// then the end-date and end-time fields should be visible again
cy.get('#end-date').should('be.visible');
cy.get('#end-time').should('be.visible');
// then the end field should be visible again
cy.get('.field-wrapper-end').should('be.visible');
cy.get('.field-wrapper-end [data-type="hour"]').should('be.visible');
});

it('As editor I can add a Link (with an external link)', function () {
Expand Down
6 changes: 3 additions & 3 deletions packages/volto/cypress/tests/minimal/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ describe('Add Content Tests', () => {
// because of lazyloading wait for the element to reach an actionable state
.clear()
.type('datetimeWidget test');
cy.get('#start-time').click();
cy.get('.rc-time-picker-panel-input').click();
cy.get('.rc-time-picker-panel-input').clear().type('6:40 AM');
// Set time using react-aria date segments
cy.get('.field-wrapper-start [data-type="hour"]').click().type('6');
cy.get('.field-wrapper-start [data-type="minute"]').click().type('40');
cy.get('#toolbar-save').click();

// then
Expand Down
1 change: 1 addition & 0 deletions packages/volto/news/8065.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use `DateTimePicker` from `@plone/components` (Quanta) for the datetime widget, replacing `react-dates`, `rc-time-picker`, and moment.js. @tedw87
3 changes: 2 additions & 1 deletion packages/volto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"@dnd-kit/core": "6.0.8",
"@dnd-kit/sortable": "7.0.2",
"@dnd-kit/utilities": "3.2.2",
"@internationalized/date": "^3.12.0",
"@loadable/component": "5.14.1",
"@loadable/server": "5.14.0",
"@plone/components": "workspace:*",
Expand Down Expand Up @@ -228,6 +229,7 @@
"@loadable/webpack-plugin": "5.15.2",
"@plone/babel-preset-razzle": "workspace:^",
"@plone/razzle": "workspace:*",
"@plone/razzle-dev-utils": "workspace:*",
"@plone/types": "workspace:*",
"@plone/volto-coresandbox": "workspace:*",
"@sinonjs/fake-timers": "^6.0.1",
Expand Down Expand Up @@ -300,7 +302,6 @@
"postcss-loader": "7.0.2",
"postcss-overrides": "3.1.4",
"postcss-scss": "4.0.6",
"@plone/razzle-dev-utils": "workspace:*",
"react-docgen-typescript-plugin": "^1.0.5",
"react-error-overlay": "6.0.9",
"react-is": "^18.2.0",
Expand Down
223 changes: 20 additions & 203 deletions packages/volto/src/components/manage/Widgets/DatetimeWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,231 +1,50 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, useIntl } from 'react-intl';
import loadable from '@loadable/component';
import cx from 'classnames';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import { DateTimePicker } from '@plone/components/quanta';
import { today, getLocalTimeZone } from '@internationalized/date';
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
import { parseDateTime, toBackendLang } from '@plone/volto/helpers/Utils/Utils';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';

import leftKey from '@plone/volto/icons/left-key.svg';
import rightKey from '@plone/volto/icons/right-key.svg';
import clearSVG from '@plone/volto/icons/clear.svg';

import 'rc-time-picker/assets/index.css';
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';

const TimePicker = loadable(() => import('rc-time-picker'));

const messages = defineMessages({
date: {
id: 'Date',
defaultMessage: 'Date',
},
time: {
id: 'Time',
defaultMessage: 'Time',
},
clearDateTime: {
id: 'Clear date/time',
defaultMessage: 'Clear date and time',
},
});

const PrevIcon = () => (
<div
style={{
color: '#000',
left: '22px',
padding: '5px',
position: 'absolute',
top: '15px',
}}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex="0"
>
<Icon name={leftKey} size="30px" />
</div>
);

const NextIcon = () => (
<div
style={{
color: '#000',
right: '22px',
padding: '5px',
position: 'absolute',
top: '15px',
}}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex="0"
>
<Icon name={rightKey} size="30px" />
</div>
);

const defaultTimeDateOnly = {
hour: 12,
minute: 0,
second: 0,
};

const DatetimeWidgetComponent = (props) => {
const {
id,
resettable,
reactDates,
widgetOptions,
moment,
value,
onChange,
dateOnly,
widget,
noPastDates: propNoPastDates,
isDisabled,
formData,
widgetOptions,
} = props;

const intl = useIntl();
const lang = intl.locale;

const [focused, setFocused] = useState(false);
const [isDefault, setIsDefault] = useState(false);

const { SingleDatePicker } = reactDates;

useEffect(() => {
const parsedDateTime = parseDateTime(
toBackendLang(lang),
value,
undefined,
moment.default,
);
setIsDefault(
parsedDateTime?.toISOString() === moment.default().utc().toISOString(),
);
}, [value, lang, moment]);

// If open_end is checked and this is the end field, don't render
if (id === 'end' && formData?.open_end) {
return null;
}

const getInternalValue = () => {
return parseDateTime(toBackendLang(lang), value, undefined, moment.default);
};

const getDateOnly = () => {
return (
dateOnly ||
widget === 'date' ||
((id === 'start' || id === 'end') && formData?.whole_day)
);
};

const onDateChange = (date) => {
if (date) {
const isDateOnly = getDateOnly();
const base = (getInternalValue() || moment.default()).set({
year: date.year(),
month: date.month(),
date: date.date(),
...(isDateOnly ? defaultTimeDateOnly : {}),
});
const dateValue = isDateOnly
? base.format('YYYY-MM-DD')
: base.toISOString();
onChange(id, dateValue);
}
setIsDefault(false);
};

const onTimeChange = (time) => {
if (time) {
const base = (getInternalValue() || moment.default()).set({
hours: time?.hours() ?? 0,
minutes: time?.minutes() ?? 0,
seconds: 0,
});
const dateValue = base.toISOString();
onChange(id, dateValue);
}
};

const onResetDates = () => {
setIsDefault(false);
onChange(id, null);
};

const onFocusChange = ({ focused }) => setFocused(focused);
const isDateOnly =
dateOnly ||
widget === 'date' ||
((id === 'start' || id === 'end') && formData?.whole_day);

const noPastDates =
propNoPastDates || widgetOptions?.pattern_options?.noPastDates;
const datetime = getInternalValue();
const isDateOnly = getDateOnly();

const handleChange = (newValue) => {
onChange(id, newValue);
};

return (
<FormFieldWrapper {...props}>
<div className="date-time-widget-wrapper">
<div
className={cx('ui input date-input', {
'default-date': isDefault,
})}
>
<SingleDatePicker
date={datetime}
disabled={isDisabled}
onDateChange={onDateChange}
focused={focused}
numberOfMonths={1}
{...(noPastDates ? {} : { isOutsideRange: () => false })}
onFocusChange={onFocusChange}
noBorder
displayFormat={moment.default
.localeData(toBackendLang(lang))
.longDateFormat('L')}
navPrev={<PrevIcon />}
navNext={<NextIcon />}
id={`${id}-date`}
placeholder={intl.formatMessage(messages.date)}
/>
</div>
{!isDateOnly && (
<div
className={cx('ui input time-input', {
'default-date': isDefault,
})}
>
<TimePicker
disabled={isDisabled}
defaultValue={datetime}
value={datetime}
onChange={onTimeChange}
allowEmpty={false}
showSecond={false}
use12Hours={lang === 'en'}
id={`${id}-time`}
format={moment.default
.localeData(toBackendLang(lang))
.longDateFormat('LT')}
placeholder={intl.formatMessage(messages.time)}
focusOnOpen
placement="bottomRight"
/>
</div>
)}
{resettable && (
<button
type="button"
disabled={isDisabled || !datetime}
onClick={onResetDates}
className="item ui noborder button"
aria-label={intl.formatMessage(messages.clearDateTime)}
>
<Icon name={clearSVG} size="24px" className="close" />
</button>
)}
<DateTimePicker
value={value}
onChange={handleChange}
granularity={isDateOnly ? 'day' : 'minute'}
isDisabled={isDisabled}
resettable={resettable}
{...(noPastDates ? { minValue: today(getLocalTimeZone()) } : {})}
/>
</div>
</FormFieldWrapper>
);
Expand Down Expand Up @@ -255,6 +74,4 @@ DatetimeWidgetComponent.defaultProps = {
resettable: true,
};

export default injectLazyLibs(['reactDates', 'moment'])(
DatetimeWidgetComponent,
);
export default DatetimeWidgetComponent;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@plone/components/dist/basic.css';
import '@plone/components/dist/quanta.css';

const InjectPloneComponentsCSS = () => {
return null;
Expand Down
Loading
Loading