Skip to content

Commit

Permalink
Post Schedule: show post events (#29716)
Browse files Browse the repository at this point in the history
* post-schedule: collect and pass calendar events

* post-schedule: re-write component using function

* date-time: remove onMonthPreviewed() and highlight

* post-schedule: update current month

* date-time: render day events using own component

* date-time: styling when not selected day

* fixing eslint issues

* date-time: tweak calendar events style

* post-schedule: exlude current post from evenys

* date-time: decouple onMonthChange with keepFocusInside

* date-time: check events before to pick events day

* fix rebasing issues

* date-time: events replace circles by dots

* fix rebasing bug

* post-schedule: split up processing post events

* date-time: revert prop name change

* date-time: add screen reader support

* date-time: update content for screen reader

* date-time: fix eslint error

* post-schedule: get events by post type

* date-time: rephrase events number for readers

* date-time: add events to aria label

* date-time: fix lint error. tidy.

* date-time: set aria-label value

* date-time: update e2e test

* date-time: clean comment

* date-time: join day-with-events string to improve its translation

* date-time: remove ref as hook dependency
The ref nevers changes its value

* post-scheudle: use title.rendered prop for events
  • Loading branch information
retrofox authored Apr 8, 2021
1 parent 444b0ae commit b5ec4ae
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 40 deletions.
93 changes: 76 additions & 17 deletions packages/components/src/date-time/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,88 @@
* External dependencies
*/
import moment from 'moment';
import classnames from 'classnames';

// react-dates doesn't tree-shake correctly, so we import from the individual
// component here, to avoid including too much of the library
import DayPickerSingleDateController from 'react-dates/lib/components/DayPickerSingleDateController';

/**
* WordPress dependencies
*/
import { Component, createRef } from '@wordpress/element';
import { isRTL } from '@wordpress/i18n';
import { Component, createRef, useEffect, useRef } from '@wordpress/element';
import { isRTL, _n, sprintf } from '@wordpress/i18n';

/**
* Module Constants
*/
const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
const ARIAL_LABEL_TIME_FORMAT = 'dddd, LL';

function DatePickerDay( { day, events = [] } ) {
const ref = useRef();

/*
* a11y hack to make the `There is/are n events` string
* available speaking for readers,
* re-defining the aria-label attribute.
* This attribute is handled by the react-dates component.
*/
useEffect( () => {
// Bail when no parent node.
if ( ! ref?.current?.parentNode ) {
return;
}

const { parentNode } = ref.current;
const dayAriaLabel = moment( day ).format( ARIAL_LABEL_TIME_FORMAT );

if ( ! events.length ) {
// Set aria-label without event description.
parentNode.setAttribute( 'aria-label', dayAriaLabel );
return;
}

const dayWithEventsDescription = sprintf(
// translators: 1: Calendar day format, 2: Calendar event number.
_n(
'%1$s. There is %2$d event.',
'%1$s. There are %2$d events.',
events.length
),
dayAriaLabel,
events.length
);

parentNode.setAttribute( 'aria-label', dayWithEventsDescription );
}, [ events.length ] );

return (
<div
ref={ ref }
className={ classnames( 'components-datetime__date__day', {
'has-events': events?.length,
} ) }
>
{ day.format( 'D' ) }
</div>
);
}

class DatePicker extends Component {
constructor() {
super( ...arguments );

this.onChangeMoment = this.onChangeMoment.bind( this );
this.nodeRef = createRef();
this.keepFocusInside = this.keepFocusInside.bind( this );
this.isDayHighlighted = this.isDayHighlighted.bind( this );
this.onMonthPreviewedHandler = this.onMonthPreviewedHandler.bind(
this
);
}

onMonthPreviewedHandler( newMonthDate ) {
this.props?.onMonthPreviewed( newMonthDate.toISOString() );
this.keepFocusInside();
}

/*
Expand Down Expand Up @@ -89,19 +148,13 @@ class DatePicker extends Component {
return currentDate ? moment( currentDate ) : moment();
}

// todo change reference to `isDayHighlighted` every time, `events` prop change
isDayHighlighted( date ) {
// Do not highlight when no events.
getEventsPerDay( day ) {
if ( ! this.props.events?.length ) {
return false;
}
if ( this.props.onMonthPreviewed ) {
this.props.onMonthPreviewed( date.toDate() );
return [];
}

// Compare date against highlighted events.
return this.props.events.some( ( highlighted ) =>
date.isSame( highlighted.date, 'day' )
return this.props.events.filter( ( eventDay ) =>
day.isSame( eventDay.date, 'day' )
);
}

Expand All @@ -126,13 +179,19 @@ class DatePicker extends Component {
onDateChange={ this.onChangeMoment }
transitionDuration={ 0 }
weekDayFormat="ddd"
dayAriaLabelFormat={ ARIAL_LABEL_TIME_FORMAT }
isRTL={ isRTL() }
isOutsideRange={ ( date ) => {
return isInvalidDate && isInvalidDate( date.toDate() );
} }
isDayHighlighted={ this.isDayHighlighted }
onPrevMonthClick={ this.keepFocusInside }
onNextMonthClick={ this.keepFocusInside }
onPrevMonthClick={ this.onMonthPreviewedHandler }
onNextMonthClick={ this.onMonthPreviewedHandler }
renderDayContents={ ( day ) => (
<DatePickerDay
day={ day }
events={ this.getEventsPerDay( day ) }
/>
) }
/>
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions packages/components/src/date-time/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Needed to initialise the default datepicker styles.
// See: https://github.com/airbnb/react-dates#initialize
import 'react-dates/initialize';
import { noop } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -25,7 +26,7 @@ function DateTimePicker(
currentDate,
is12Hour,
isInvalidDate,
onMonthPreviewed,
onMonthPreviewed = noop,
onChange,
events,
},
Expand All @@ -52,8 +53,8 @@ function DateTimePicker(
currentDate={ currentDate }
onChange={ onChange }
isInvalidDate={ isInvalidDate }
onMonthPreviewed={ onMonthPreviewed }
events={ events }
onMonthPreviewed={ onMonthPreviewed }
/>
</>
) }
Expand Down
25 changes: 25 additions & 0 deletions packages/components/src/date-time/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,31 @@
}
}

.components-datetime__date .CalendarDay .components-datetime__date__day {
height: 100%;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
position: relative;

&.has-events::before {
content: " ";
width: 4px;
height: 4px;
border-radius: 2px;
position: absolute;
left: 50%;
margin-left: -2px;
bottom: 0;
background-color: $white;
}
}

.components-datetime__date .CalendarDay:not(.CalendarDay__selected) .components-datetime__date__day.has-events::before {
background: var(--wp-admin-theme-color);
}

.components-datetime__time {
padding-bottom: $grid-unit-20;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe( 'Post visibility', () => {
);
await (
await page.$x(
'//td[contains(concat(" ", @class, " "), " CalendarDay ")][text() = "15"]'
'//td[contains(concat(" ", @class, " "), " CalendarDay ")]/div[contains(concat(" ", @class, " "), " components-datetime__date__day ")][text() = "15"]'
)
)[ 0 ].click();

Expand Down
81 changes: 61 additions & 20 deletions packages/editor/src/components/post-schedule/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,68 @@
* WordPress dependencies
*/
import { __experimentalGetSettings } from '@wordpress/date';
import { withSelect, withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { useDispatch, useSelect } from '@wordpress/data';
import { DateTimePicker } from '@wordpress/components';
import { useRef } from '@wordpress/element';
import { useRef, useState, useMemo } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';

function getDayOfTheMonth( date = new Date(), firstDay = true ) {
const d = new Date( date );
return new Date(
d.getFullYear(),
d.getMonth() + ( firstDay ? 0 : 1 ),
firstDay ? 1 : 0
).toISOString();
}

export default function PostSchedule() {
const { postDate, postType } = useSelect(
( select ) => ( {
postDate: select( editorStore ).getEditedPostAttribute( 'date' ),
postType: select( editorStore ).getCurrentPostType(),
} ),
[]
);

const { editPost } = useDispatch( editorStore );
const onUpdateDate = ( date ) => editPost( { date } );

const [ previewedMonth, setPreviewedMonth ] = useState(
getDayOfTheMonth( postDate )
);

// Pick up published and schduled site posts.
const eventsByPostType = useSelect(
( select ) =>
select( coreStore ).getEntityRecords( 'postType', postType, {
status: 'publish,future',
after: getDayOfTheMonth( previewedMonth ),
before: getDayOfTheMonth( previewedMonth, false ),
exclude: [ select( editorStore ).getCurrentPostId() ],
} ),
[ previewedMonth, postType ]
);

const events = useMemo(
() =>
( eventsByPostType || [] ).map(
( { title, type, date: eventDate } ) => ( {
title: title?.rendered,
type,
date: new Date( eventDate ),
} )
),
[ eventsByPostType ]
);

export function PostSchedule( { date, onUpdateDate } ) {
const ref = useRef();
const settings = __experimentalGetSettings();

// To know if the current timezone is a 12 hour time with look for "a" in the time format
// We also make sure this a is not escaped by a "/"
const is12HourTime = /a(?!\\)/i.test(
Expand All @@ -30,24 +84,11 @@ export function PostSchedule( { date, onUpdateDate } ) {
return (
<DateTimePicker
ref={ ref }
currentDate={ date }
currentDate={ postDate }
onChange={ onChange }
is12Hour={ is12HourTime }
events={ events }
onMonthPreviewed={ setPreviewedMonth }
/>
);
}

export default compose( [
withSelect( ( select ) => {
return {
date: select( 'core/editor' ).getEditedPostAttribute( 'date' ),
};
} ),
withDispatch( ( dispatch ) => {
return {
onUpdateDate( date ) {
dispatch( 'core/editor' ).editPost( { date } );
},
};
} ),
] )( PostSchedule );

0 comments on commit b5ec4ae

Please sign in to comment.