From 36871bf509603dd05ad16f6cb9c5f5d9517cae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arturo=20Forn=C3=A9s=20Arvayo?= Date: Fri, 2 Jun 2023 14:10:19 -0600 Subject: [PATCH] feat(allDayMaxRows): Allow for more granular control Allows for more granular control over the number of events display in the all day row at the top of the TimeGrid Co-authored-by: Arturo Fornes Closes #2386 --- src/BackgroundCells.js | 4 +- src/Calendar.js | 10 +++ src/Day.js | 56 +++++++++++- src/EventEndingRow.js | 2 +- src/Selection.js | 23 +++-- src/TimeGrid.js | 116 ++++++++++++++++++++++++- src/TimeGridHeader.js | 9 ++ src/Week.js | 57 +++++++++++- src/utils/DateSlotMetrics.js | 5 +- stories/props/API.stories.mdx | 10 +++ stories/props/allDayMaxRows.mdx | 10 +++ stories/props/allDayMaxRows.stories.js | 39 +++++++++ stories/resources/allDayEvents.js | 44 ++++++++++ 13 files changed, 369 insertions(+), 16 deletions(-) create mode 100644 stories/props/allDayMaxRows.mdx create mode 100644 stories/props/allDayMaxRows.stories.js create mode 100644 stories/resources/allDayEvents.js diff --git a/src/BackgroundCells.js b/src/BackgroundCells.js index 56d94f4d7..50262f11a 100644 --- a/src/BackgroundCells.js +++ b/src/BackgroundCells.js @@ -4,7 +4,7 @@ import clsx from 'clsx' import { notify } from './utils/helpers' import { dateCellSelection, getSlotAtX, pointInBox } from './utils/selection' -import Selection, { getBoundsForNode, isEvent } from './Selection' +import Selection, { getBoundsForNode, isEvent, isShowMore } from './Selection' class BackgroundCells extends React.Component { constructor(props, context) { @@ -77,7 +77,7 @@ class BackgroundCells extends React.Component { })) let selectorClicksHandler = (point, actionType) => { - if (!isEvent(node, point)) { + if (!isEvent(node, point) && !isShowMore(node, point)) { let rowBox = getBoundsForNode(node) let { range, rtl } = this.props diff --git a/src/Calendar.js b/src/Calendar.js index f008227c4..ec3a7344f 100644 --- a/src/Calendar.js +++ b/src/Calendar.js @@ -603,6 +603,15 @@ class Calendar extends React.Component { */ showMultiDayTimes: PropTypes.bool, + /** + * Determines a maximum amount of rows of events to display in the all day + * section for Week and Day views, will display `showMore` button if + * events excede this number. + * + * Defaults to `Infinity` + */ + allDayMaxRows: PropTypes.number, + /** * Constrains the minimum _time_ of the Day and Week views. */ @@ -865,6 +874,7 @@ class Calendar extends React.Component { views: [views.MONTH, views.WEEK, views.DAY, views.AGENDA], step: 30, length: 30, + allDayMaxRows: Infinity, doShowMoreDrillDown: true, drilldownView: views.DAY, diff --git a/src/Day.js b/src/Day.js index f301eb7a5..6eeb4feb1 100644 --- a/src/Day.js +++ b/src/Day.js @@ -2,6 +2,8 @@ import PropTypes from 'prop-types' import React from 'react' import { navigate } from './utils/constants' +import { DayLayoutAlgorithmPropType } from './utils/propTypes' + import TimeGrid from './TimeGrid' class Day extends React.Component { @@ -39,11 +41,63 @@ class Day extends React.Component { Day.propTypes = { date: PropTypes.instanceOf(Date).isRequired, - localizer: PropTypes.any, + + events: PropTypes.array.isRequired, + backgroundEvents: PropTypes.array.isRequired, + resources: PropTypes.array, + + step: PropTypes.number, + timeslots: PropTypes.number, + range: PropTypes.arrayOf(PropTypes.instanceOf(Date)), min: PropTypes.instanceOf(Date), max: PropTypes.instanceOf(Date), + getNow: PropTypes.func.isRequired, + scrollToTime: PropTypes.instanceOf(Date), enableAutoScroll: PropTypes.bool, + showMultiDayTimes: PropTypes.bool, + + rtl: PropTypes.bool, + resizable: PropTypes.bool, + width: PropTypes.number, + + accessors: PropTypes.object.isRequired, + components: PropTypes.object.isRequired, + getters: PropTypes.object.isRequired, + localizer: PropTypes.object.isRequired, + + allDayMaxRows: PropTypes.number, + + selected: PropTypes.object, + selectable: PropTypes.oneOf([true, false, 'ignoreEvents']), + longPressThreshold: PropTypes.number, + + onNavigate: PropTypes.func, + onSelectSlot: PropTypes.func, + onSelectEnd: PropTypes.func, + onSelectStart: PropTypes.func, + onSelectEvent: PropTypes.func, + onDoubleClickEvent: PropTypes.func, + onKeyPressEvent: PropTypes.func, + onShowMore: PropTypes.func, + onDrillDown: PropTypes.func, + getDrilldownView: PropTypes.func.isRequired, + + dayLayoutAlgorithm: DayLayoutAlgorithmPropType, + + showAllEvents: PropTypes.bool, + doShowMoreDrillDown: PropTypes.bool, + + popup: PropTypes.bool, + handleDragStart: PropTypes.func, + + popupOffset: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + ]), } Day.range = (date, { localizer }) => { diff --git a/src/EventEndingRow.js b/src/EventEndingRow.js index 79d9ac94e..fd2d8a699 100644 --- a/src/EventEndingRow.js +++ b/src/EventEndingRow.js @@ -83,7 +83,7 @@ class EventEndingRow extends React.Component { type="button" key={'sm_' + slot} className={clsx('rbc-button-link', 'rbc-show-more')} - onClick={e => this.showMore(slot, e)} + onClick={(e) => this.showMore(slot, e)} > {localizer.messages.showMore(count)} diff --git a/src/Selection.js b/src/Selection.js index b2714c17e..015691c06 100644 --- a/src/Selection.js +++ b/src/Selection.js @@ -15,10 +15,19 @@ export function getEventNodeFromPoint(node, { clientX, clientY }) { return closest(target, '.rbc-event', node) } +export function getShowMoreNodeFromPoint(node, { clientX, clientY }) { + let target = document.elementFromPoint(clientX, clientY) + return closest(target, '.rbc-show-more', node) +} + export function isEvent(node, bounds) { return !!getEventNodeFromPoint(node, bounds) } +export function isShowMore(node, bounds) { + return !!getShowMoreNodeFromPoint(node, bounds) +} + function getEventCoordinates(e) { let target = e @@ -38,7 +47,10 @@ const clickTolerance = 5 const clickInterval = 250 class Selection { - constructor(node, { global = false, longPressThreshold = 250, validContainers = [] } = {}) { + constructor( + node, + { global = false, longPressThreshold = 250, validContainers = [] } = {} + ) { this.isDetached = false this.container = node this.globalMouse = !node || global @@ -307,15 +319,14 @@ class Selection { // Check whether provided event target element // - is contained within a valid container _isWithinValidContainer(e) { - const eventTarget = e.target; - const containers = this.validContainers; + const eventTarget = e.target + const containers = this.validContainers if (!containers || !containers.length || !eventTarget) { - return true; + return true } - return containers.some( - (target) => !!eventTarget.closest(target)); + return containers.some((target) => !!eventTarget.closest(target)) } _handleTerminatingEvent(e) { diff --git a/src/TimeGrid.js b/src/TimeGrid.js index 197037a1e..79d991ac4 100644 --- a/src/TimeGrid.js +++ b/src/TimeGrid.js @@ -6,11 +6,14 @@ import memoize from 'memoize-one' import DayColumn from './DayColumn' import TimeGutter from './TimeGutter' +import TimeGridHeader from './TimeGridHeader' +import PopOverlay from './PopOverlay' import getWidth from 'dom-helpers/width' -import TimeGridHeader from './TimeGridHeader' -import { notify } from './utils/helpers' +import getPosition from 'dom-helpers/position' +import { views } from './utils/constants' import { inRange, sortEvents } from './utils/eventLevels' +import { notify } from './utils/helpers' import Resources from './utils/Resources' import { DayLayoutAlgorithmPropType } from './utils/propTypes' @@ -22,6 +25,7 @@ export default class TimeGrid extends Component { this.scrollRef = React.createRef() this.contentRef = React.createRef() + this.containerRef = React.createRef() this._scrollRatio = null this.gutterRef = createRef() } @@ -67,12 +71,50 @@ export default class TimeGrid extends Component { this.applyScroll() } - handleSelectAlldayEvent = (...args) => { + handleKeyPressEvent = (...args) => { + this.clearSelection() + notify(this.props.onKeyPressEvent, args) + } + + handleSelectEvent = (...args) => { //cancel any pending selections so only the event click goes through. this.clearSelection() notify(this.props.onSelectEvent, args) } + handleDoubleClickEvent = (...args) => { + this.clearSelection() + notify(this.props.onDoubleClickEvent, args) + } + + handleShowMore = (events, date, cell, slot, target) => { + const { + popup, + onDrillDown, + onShowMore, + getDrilldownView, + doShowMoreDrillDown, + } = this.props + this.clearSelection() + + if (popup) { + let position = getPosition(cell, this.containerRef.current) + + this.setState({ + overlay: { + date, + events, + position: { ...position, width: '200px' }, + target, + }, + }) + } else if (doShowMoreDrillDown) { + notify(onDrillDown, [date, getDrilldownView(date) || views.DAY]) + } + + notify(onShowMore, [events, date, slot]) + } + handleSelectAllDaySlot = (slots, slotInfo) => { const { onSelectSlot } = this.props @@ -202,6 +244,7 @@ export default class TimeGrid extends Component { 'rbc-time-view', resources && 'rbc-time-view-resources' )} + ref={this.containerRef} > + {this.props.popup && this.renderOverlay()}
this.setState({ overlay: null }) + + return ( + + ) + } + + overlayDisplay = () => { + this.setState({ + overlay: null, + }) + } + clearSelection() { clearTimeout(this._selectTimer) this._pendingSelection = [] @@ -341,6 +432,8 @@ TimeGrid.propTypes = { getters: PropTypes.object.isRequired, localizer: PropTypes.object.isRequired, + allDayMaxRows: PropTypes.number, + selected: PropTypes.object, selectable: PropTypes.oneOf([true, false, 'ignoreEvents']), longPressThreshold: PropTypes.number, @@ -350,12 +443,27 @@ TimeGrid.propTypes = { onSelectEnd: PropTypes.func, onSelectStart: PropTypes.func, onSelectEvent: PropTypes.func, + onShowMore: PropTypes.func, onDoubleClickEvent: PropTypes.func, onKeyPressEvent: PropTypes.func, onDrillDown: PropTypes.func, getDrilldownView: PropTypes.func.isRequired, dayLayoutAlgorithm: DayLayoutAlgorithmPropType, + + showAllEvents: PropTypes.bool, + doShowMoreDrillDown: PropTypes.bool, + + popup: PropTypes.bool, + handleDragStart: PropTypes.func, + + popupOffset: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + ]), } TimeGrid.defaultProps = { diff --git a/src/TimeGridHeader.js b/src/TimeGridHeader.js index 73cb5c924..ac76f73eb 100644 --- a/src/TimeGridHeader.js +++ b/src/TimeGridHeader.js @@ -85,6 +85,8 @@ class TimeGridHeader extends React.Component { rtl={rtl} getNow={getNow} minRows={2} + // Add +1 to include showMore button row in the row limit + maxRows={this.props.allDayMaxRows + 1} range={range} events={eventsToDisplay} resourceId={resourceId} @@ -96,6 +98,7 @@ class TimeGridHeader extends React.Component { getters={getters} localizer={localizer} onSelect={this.props.onSelectEvent} + onShowMore={this.props.onShowMore} onDoubleClick={this.props.onDoubleClickEvent} onKeyPress={this.props.onKeyPressEvent} onSelectSlot={this.props.onSelectSlot} @@ -172,6 +175,8 @@ class TimeGridHeader extends React.Component { rtl={rtl} getNow={getNow} minRows={2} + // Add +1 to include showMore button row in the row limit + maxRows={this.props.allDayMaxRows + 1} range={range} events={groupedEvents.get(id) || []} resourceId={resource && id} @@ -183,6 +188,7 @@ class TimeGridHeader extends React.Component { getters={getters} localizer={localizer} onSelect={this.props.onSelectEvent} + onShowMore={this.props.onShowMore} onDoubleClick={this.props.onDoubleClickEvent} onKeyPress={this.props.onKeyPressEvent} onSelectSlot={this.props.onSelectSlot} @@ -216,11 +222,14 @@ TimeGridHeader.propTypes = { selectable: PropTypes.oneOf([true, false, 'ignoreEvents']), longPressThreshold: PropTypes.number, + allDayMaxRows: PropTypes.number, + onSelectSlot: PropTypes.func, onSelectEvent: PropTypes.func, onDoubleClickEvent: PropTypes.func, onKeyPressEvent: PropTypes.func, onDrillDown: PropTypes.func, + onShowMore: PropTypes.func, getDrilldownView: PropTypes.func.isRequired, scrollRef: PropTypes.any, } diff --git a/src/Week.js b/src/Week.js index 1e91d307f..b0987834d 100644 --- a/src/Week.js +++ b/src/Week.js @@ -1,6 +1,9 @@ import PropTypes from 'prop-types' import React from 'react' + import { navigate } from './utils/constants' +import { DayLayoutAlgorithmPropType } from './utils/propTypes' + import TimeGrid from './TimeGrid' class Week extends React.Component { @@ -38,11 +41,63 @@ class Week extends React.Component { Week.propTypes = { date: PropTypes.instanceOf(Date).isRequired, - localizer: PropTypes.any, + + events: PropTypes.array.isRequired, + backgroundEvents: PropTypes.array.isRequired, + resources: PropTypes.array, + + step: PropTypes.number, + timeslots: PropTypes.number, + range: PropTypes.arrayOf(PropTypes.instanceOf(Date)), min: PropTypes.instanceOf(Date), max: PropTypes.instanceOf(Date), + getNow: PropTypes.func.isRequired, + scrollToTime: PropTypes.instanceOf(Date), enableAutoScroll: PropTypes.bool, + showMultiDayTimes: PropTypes.bool, + + rtl: PropTypes.bool, + resizable: PropTypes.bool, + width: PropTypes.number, + + accessors: PropTypes.object.isRequired, + components: PropTypes.object.isRequired, + getters: PropTypes.object.isRequired, + localizer: PropTypes.object.isRequired, + + allDayMaxRows: PropTypes.number, + + selected: PropTypes.object, + selectable: PropTypes.oneOf([true, false, 'ignoreEvents']), + longPressThreshold: PropTypes.number, + + onNavigate: PropTypes.func, + onSelectSlot: PropTypes.func, + onSelectEnd: PropTypes.func, + onSelectStart: PropTypes.func, + onSelectEvent: PropTypes.func, + onDoubleClickEvent: PropTypes.func, + onKeyPressEvent: PropTypes.func, + onShowMore: PropTypes.func, + onDrillDown: PropTypes.func, + getDrilldownView: PropTypes.func.isRequired, + + dayLayoutAlgorithm: DayLayoutAlgorithmPropType, + + showAllEvents: PropTypes.bool, + doShowMoreDrillDown: PropTypes.bool, + + popup: PropTypes.bool, + handleDragStart: PropTypes.func, + + popupOffset: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + x: PropTypes.number, + y: PropTypes.number, + }), + ]), } Week.defaultProps = TimeGrid.defaultProps diff --git a/src/utils/DateSlotMetrics.js b/src/utils/DateSlotMetrics.js index fb2e7ee70..bc571d3d2 100644 --- a/src/utils/DateSlotMetrics.js +++ b/src/utils/DateSlotMetrics.js @@ -16,7 +16,10 @@ export function getSlotMetrics() { ) let { levels, extra } = eventLevels(segments, Math.max(maxRows - 1, 1)) - while (levels.length < minRows) levels.push([]) + // Subtract 1 from minRows to not include showMore button row when + // it would be rendered + const minEventRows = extra.length > 0 ? minRows - 1 : minRows + while (levels.length < minEventRows) levels.push([]) return { first, diff --git a/stories/props/API.stories.mdx b/stories/props/API.stories.mdx index e1befb987..76506b668 100644 --- a/stories/props/API.stories.mdx +++ b/stories/props/API.stories.mdx @@ -257,6 +257,16 @@ The end date/time of the event. Must resolve to a JavaScript `Date` object. Determines whether the event should be considered an "all day" event and ignore time. Must resolve to a `boolean` value. +### allDayMaxRows + +- type: `number` +- default: `Infinity` +- + Example + + +Determines a maximum amount of rows of events to display in the all day section for Week and Day views, will display `showMore` button if events excede this number. + ### resources - type: `arrayOf(Resource)` diff --git a/stories/props/allDayMaxRows.mdx b/stories/props/allDayMaxRows.mdx new file mode 100644 index 000000000..e6a9c5dbc --- /dev/null +++ b/stories/props/allDayMaxRows.mdx @@ -0,0 +1,10 @@ +import { Canvas, Story } from '@storybook/addon-docs' + +# allDayMaxRows + +- type: `number` +- default: `Infinity` + +Determines a maximum amount of rows of events to display in the all day section for Week and Day views, will display `showMore` button if events excede this number. + + diff --git a/stories/props/allDayMaxRows.stories.js b/stories/props/allDayMaxRows.stories.js new file mode 100644 index 000000000..db189732d --- /dev/null +++ b/stories/props/allDayMaxRows.stories.js @@ -0,0 +1,39 @@ +import React from 'react' +import moment from 'moment' +import { Calendar, Views, momentLocalizer } from '../../src' +import allDayEvents from '../resources/allDayEvents' +import mdx from './allDayMaxRows.mdx' + +const mLocalizer = momentLocalizer(moment) + +export default { + title: 'props', + component: Calendar, + argTypes: { + localizer: { control: { type: null } }, + events: { control: { type: null } }, + defaultDate: { control: { type: null } }, + }, + parameters: { + docs: { + page: mdx, + }, + }, +} + +const Template = (args) => ( +
+ +
+) + +export const AllDayMaxRows = Template.bind({}) +AllDayMaxRows.storyName = 'allDayMaxRows' +AllDayMaxRows.args = { + defaultDate: new Date(2015, 3, 1), + defaultView: Views.WEEK, + events: allDayEvents, + localizer: mLocalizer, + allDayMaxRows: 2, + popup: true, +} diff --git a/stories/resources/allDayEvents.js b/stories/resources/allDayEvents.js new file mode 100644 index 000000000..842991879 --- /dev/null +++ b/stories/resources/allDayEvents.js @@ -0,0 +1,44 @@ +export default [ + { + id: 0, + title: 'All Day Event very long title', + allDay: true, + start: new Date(2015, 3, 0), + end: new Date(2015, 3, 1), + }, + { + id: 1, + title: '#2 All Day Event very long title', + allDay: true, + start: new Date(2015, 3, 0), + end: new Date(2015, 3, 2), + }, + { + id: 2, + title: '#3 All Day Event very long title', + allDay: true, + start: new Date(2015, 3, 0), + end: new Date(2015, 3, 1), + }, + { + id: 3, + title: '#4 All Day Event', + allDay: true, + start: new Date(2015, 3, 0), + end: new Date(2015, 3, 1), + }, + { + id: 4, + title: '#5 All Day Event', + allDay: true, + start: new Date(2015, 3, 0), + end: new Date(2015, 3, 1), + }, + { + id: 5, + title: '#6 All Day Event', + allDay: true, + start: new Date(2015, 3, 7), + end: new Date(2015, 3, 7), + }, +]