Converted to hooks

This commit is contained in:
Hugo Bernier 2020-04-18 18:30:30 -04:00
parent 3d0700133a
commit aa008a169a
5 changed files with 203 additions and 259 deletions

View File

@ -3,90 +3,70 @@ import { css } from "office-ui-fabric-react/lib/Utilities";
import * as React from "react";
import styles from "./DateBox.module.scss";
import { DateBoxSize, IDateBoxProps } from ".";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
/**
* Shows a date in a SharePoint-looking date
*/
export class DateBox extends React.Component<IDateBoxProps, {}> {
public render(): React.ReactElement<IDateBoxProps> {
// convert start and end date into moments so that we can manipulate them
const startMoment: moment.Moment = moment(this.props.startDate);
export const DateBox = (props: IDateBoxProps) => {
// convert start and end date into moments so that we can manipulate them
const startMoment: moment.Moment = moment(props.startDate);
// event actually ends one second before the end date
const endMoment: moment.Moment = moment(this.props.endDate).add(-1, "s");
// event actually ends one second before the end date
const endMoment: moment.Moment = moment(props.endDate).add(-1, "s");
// check if both dates are on the same day
const isSameDay: boolean = startMoment.isSame(endMoment, "day");
if (isSameDay) {
return this._renderSingleDay(startMoment);
} else {
return this._renderMultiDay(startMoment, endMoment);
}
}
/**
* Renders an event that happens in a single day
* @param startMoment
*/
private _renderSingleDay(startMoment: moment.Moment): JSX.Element {
const { className, size, themeVariant } = this.props;
// check if both dates are on the same day
const isSameDay: boolean = startMoment.isSame(endMoment, "day");
if (isSameDay) {
return (
<div className={css(styles.box,
styles.boxIsSingleDay,
(size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), className)}
(props.size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), props.className)}
style={
themeVariant &&
props.themeVariant &&
{
// KLUDGE: It seems like the themeVariant palette doesn't expose primaryBackground
backgroundColor: themeVariant.palette["primaryBackground"],
borderColor: themeVariant.semanticColors.bodyDivider
backgroundColor: props.themeVariant.palette["primaryBackground"],
borderColor: props.themeVariant.semanticColors.bodyDivider
}}>
<div className={styles.month}
style={
themeVariant &&
{ color: themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM").toUpperCase()}</div>
props.themeVariant &&
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM").toUpperCase()}</div>
<div className={styles.day}
style={
themeVariant &&
{ color: themeVariant.semanticColors.bodyText }}>{startMoment.format("D")}</div>
props.themeVariant &&
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("D")}</div>
</div>);
}
/**
* Renders an event that spans over multiple days
* @param startMoment
* @param endMoment
*/
private _renderMultiDay(startMoment: moment.Moment, endMoment: moment.Moment): JSX.Element {
const { className, size, themeVariant } = this.props;
} else {
return (
<div
className={css(styles.box,
styles.boxIsSingleDay,
(size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), className)}
(props.size === DateBoxSize.Small ? styles.boxIsSmall : styles.boxIsMedium), props.className)}
style={
themeVariant &&
props.themeVariant &&
{
backgroundColor: themeVariant.palette["primaryBackground"],
borderColor: themeVariant.semanticColors.bodyDivider
backgroundColor: props.themeVariant.palette["primaryBackground"],
borderColor: props.themeVariant.semanticColors.bodyDivider
}}>
<div className={styles.date} style={
themeVariant &&
{ color: themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM D").toUpperCase()}</div>
props.themeVariant &&
{ color: props.themeVariant.semanticColors.bodyText }}>{startMoment.format("MMM D").toUpperCase()}</div>
<hr className={styles.separator}
style={
themeVariant &&
props.themeVariant &&
{
borderColor: themeVariant.semanticColors.bodyText
borderColor: props.themeVariant.semanticColors.bodyText
}}
/>
<div className={styles.date} style={
themeVariant &&
{ color: themeVariant.semanticColors.bodyText }}>{endMoment.format("MMM D").toUpperCase()}</div>
props.themeVariant &&
{ color: props.themeVariant.semanticColors.bodyText }}>{endMoment.format("MMM D").toUpperCase()}</div>
</div>);
}
}
};

View File

@ -8,149 +8,30 @@ import { IEventCardProps } from ".";
import { DateBox, DateBoxSize } from "../DateBox";
import styles from "./EventCard.module.scss";
import { Text } from "@microsoft/sp-core-library";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import { useCallback } from 'react';
/**
* Shows an event in a document card
*/
export class EventCard extends React.Component<IEventCardProps, {}> {
public render(): React.ReactElement<IEventCardProps> {
const { isNarrow } = this.props;
export const EventCard = (props: IEventCardProps) => {
const { isNarrow, themeVariant, isEditMode, event } = props;
if (isNarrow) {
return this._renderNarrowCell();
} else {
return this._renderNormalCell();
}
}
// Get the cell information
const { start,
end,
allDay,
title,
url,
category,
location
} = event;
/**
* Renders a full width cell
*/
private _renderNormalCell(): JSX.Element {
const { themeVariant } = this.props;
const { start,
end,
allDay,
title,
url,
category,
// description,
location } = this.props.event;
const eventDate: moment.Moment = moment(start);
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
const { isEditMode } = this.props;
return (
<div>
<div
className={css(styles.cardWrapper)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, `${dateString}`)}
tabIndex={0}
>
<DocumentCard
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
type={DocumentCardType.normal}
onClickHref={isEditMode ? null : url}
style={themeVariant && { borderColor: themeVariant.semanticColors.bodyDivider }}
>
<FocusZone>
<div className={styles.dateBoxContainer} style={{ height: 160 }}>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Medium}
themeVariant={themeVariant}
/>
</div>
<div className={styles.detailsContainer}>
<div className={styles.category} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext}}>{category}</div>
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText}}>{title}</div>
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext}}>{dateString}</div>
<div className={styles.location} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext}}>{location}</div>
<ActionButton
className={styles.addToMyCalendar}
style={themeVariant && { color: themeVariant.semanticColors.bodyText}}
iconProps={{ iconName: "AddEvent" }}
ariaLabel={strings.AddToCalendarAriaLabel}
onClick={this._onAddToMyCalendar}
>
{strings.AddToCalendarButtonLabel}
</ActionButton>
</div>
</FocusZone>
</DocumentCard>
</div>
</div>
);
}
/**
* Renders a narrow event card cell
*/
private _renderNarrowCell(): JSX.Element {
// Get the cell information
const { start,
end,
allDay,
title,
url,
} = this.props.event;
const { themeVariant } = this.props;
// Calculate the date and string format
const eventDate: moment.Moment = moment(start);
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
// Define theme variant styles if themevariant was passed
return (
<div>
<div
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, dateString)}
>
<DocumentCard
className={css(styles.root, styles.rootIsActionable, styles.rootIsCompact)}
type={DocumentCardType.compact}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
onClickHref={url}
>
<div>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Small}
themeVariant={themeVariant}
/>
</div>
<div>
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText}}>{title}</div>
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext}}>{dateString}</div>
</div>
</DocumentCard>
</div>
</div>
);
}
const eventDate: moment.Moment = moment(start);
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
/**
* Handle adding to calendar
*/
private _onAddToMyCalendar = (): void => {
const { event } = this.props;
const _onAddToMyCalendar = useCallback((): void => {
// create a calendar to hold the event
const cal: ICS.VCALENDAR = new ICS.VCALENDAR();
@ -198,5 +79,92 @@ export class EventCard extends React.Component<IEventCardProps, {}> {
// my spidey senses are telling me that there are sitaations where this isn't going to work, but none of my tests could prove it.
// i suspect we're not encoding events properly
window.open("data:text/calendar;charset=utf8," + encodeURIComponent(cal.toString()));
}, [event]);
if (isNarrow) {
// Calculate the date and string format
// Define theme variant styles if themevariant was passed
return (
<div>
<div
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, dateString)}
>
<DocumentCard
className={css(styles.root, styles.rootIsActionable, styles.rootIsCompact)}
type={DocumentCardType.compact}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
onClickHref={url}
>
<div>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Small}
themeVariant={themeVariant}
/>
</div>
<div>
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText }}>{title}</div>
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{dateString}</div>
</div>
</DocumentCard>
</div>
</div>
);
} else {
return (
<div>
<div
className={css(styles.cardWrapper)}
style={themeVariant && { backgroundColor: themeVariant.semanticColors.bodyBackground }}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, `${dateString}`)}
tabIndex={0}
>
<DocumentCard
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
type={DocumentCardType.normal}
onClickHref={isEditMode ? null : url}
style={themeVariant && { borderColor: themeVariant.semanticColors.bodyDivider }}
>
<FocusZone>
<div className={styles.dateBoxContainer} style={{ height: 160 }}>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Medium}
themeVariant={themeVariant}
/>
</div>
<div className={styles.detailsContainer}>
<div className={styles.category} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{category}</div>
<div className={styles.title} style={themeVariant && { color: themeVariant.semanticColors.bodyText }}>{title}</div>
<div className={styles.datetime} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{dateString}</div>
<div className={styles.location} style={themeVariant && { color: themeVariant.semanticColors.bodySubtext }}>{location}</div>
<ActionButton
className={styles.addToMyCalendar}
style={themeVariant && { color: themeVariant.semanticColors.bodyText }}
iconProps={{ iconName: "AddEvent" }}
ariaLabel={strings.AddToCalendarAriaLabel}
onClick={_onAddToMyCalendar}
>
{strings.AddToCalendarButtonLabel}
</ActionButton>
</div>
</FocusZone>
</DocumentCard>
</div>
</div>
);
}
}
};

View File

@ -5,84 +5,72 @@ import * as React from "react";
import { IPaginationProps } from ".";
import styles from "./Pagination.module.scss";
import * as strings from "CalendarFeedSummaryWebPartStrings";
import { useCallback } from 'react';
/**
* A custom pagination control designed to look & feel like Office UI Fabric
*/
export class Pagination extends React.Component<IPaginationProps, {}> {
public render(): React.ReactElement<IPaginationProps> {
export const Pagination = (props: IPaginationProps) => {
const { currentPage, totalItems, itemsCountPerPage } = props;
const { currentPage } = this.props;
// calculate the page situation
const numberOfPages: number = Math.round(totalItems / itemsCountPerPage);
// calculate the page situation
const numberOfPages: number = this._getNumberOfPages();
// we disable the previous button if we're on page 1
const prevDisabled: boolean = currentPage <= 1;
// we disable the previous button if we're on page 1
const prevDisabled: boolean = currentPage <= 1;
// we disable the next button if we're on the last page
const nextDisabled: boolean = currentPage >= numberOfPages;
// we disable the next button if we're on the last page
const nextDisabled: boolean = currentPage >= numberOfPages;
return (
<div className={css(styles.Pagination, this.props.showPageNum ? null : styles.noPageNum)}>
<ActionButton className={styles.prev}
data-automation-id="previousPage"
onRenderIcon={(_props: IButtonProps) => {
// we use the render custom icon method to render the icon consistently with the right icon
return (
<Icon iconName="ChevronLeft" />
);
}}
disabled={prevDisabled}
onClick={this._prevPage}
ariaLabel={strings.PrevButtonAriaLabel}
>
{strings.PrevButtonLabel}
</ActionButton>
{/* NOT IMPLEMENTED: Page numbers aren't shown here, but we'll need them if we want this control to be reusable */}
<ActionButton className={styles.next}
data-automation-id="nextPage"
disabled={nextDisabled}
onRenderMenuIcon={(props: IButtonProps) => {
// we use the render custom menu icon method to render the icon to the right of the text
return (
<Icon iconName="ChevronRight" />
);
}}
onClick={this._nextPage}
ariaLabel={strings.NextButtonAriaLabel}
>
{strings.NextButtonLabel}
</ActionButton>
</div>
);
/**
* Increments the page number unless we're on the last page
*/
const _nextPage = useCallback((): void => {
if (props.currentPage < numberOfPages) {
props.onPageUpdate(props.currentPage + 1);
}
}, [props, numberOfPages]);
/**
* Increments the page number unless we're on the last page
*/
private _nextPage = (): void => {
const numberOfPages: number = this._getNumberOfPages();
if (this.props.currentPage < numberOfPages) {
this.props.onPageUpdate(this.props.currentPage + 1);
}
/**
* Decrements the page number unless we're on the first page
*/
const _prevPage = useCallback((): void => {
if (props.currentPage > 1) {
props.onPageUpdate(props.currentPage - 1);
}
}, [props]);
/**
* Decrements the page number unless we're on the first page
*/
private _prevPage = (): void => {
if (this.props.currentPage > 1) {
this.props.onPageUpdate(this.props.currentPage - 1);
}
}
/**
* Calculates how many pages there will be
*/
private _getNumberOfPages(): number {
const { totalItems, itemsCountPerPage } = this.props;
let numPages: number = Math.round(totalItems / itemsCountPerPage);
return numPages;
}
}
return (
<div className={css(styles.Pagination, props.showPageNum ? null : styles.noPageNum)}>
<ActionButton className={styles.prev}
onRenderIcon={(_props: IButtonProps) => {
// we use the render custom icon method to render the icon consistently with the right icon
return (
<Icon iconName="ChevronLeft" />
);
}}
disabled={prevDisabled}
onClick={_prevPage}
ariaLabel={strings.PrevButtonAriaLabel}
>
{strings.PrevButtonLabel}
</ActionButton>
{/* NOT IMPLEMENTED: Page numbers aren't shown here, but we'll need them if we want this control to be reusable */}
<ActionButton className={styles.next}
data-automation-id="nextPage"
disabled={nextDisabled}
onRenderMenuIcon={(_props: IButtonProps) => {
// we use the render custom menu icon method to render the icon to the right of the text
return (
<Icon iconName="ChevronRight" />
);
}}
onClick={_nextPage}
ariaLabel={strings.NextButtonAriaLabel}
>
{strings.NextButtonLabel}
</ActionButton>
</div>
);
};

View File

@ -16,14 +16,12 @@ function useBreakpoints(currentWidth: number, breakpoints: number[]) {
* Presents the child compoments as a slick slide
*/
export const FilmstripLayout = (props: { children: any; clientWidth: number; themeVariant?: IReadonlyTheme, ariaLabel?: string; }) => {
let ref: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
// // the slick slider used in normal views
let _slider: React.MutableRefObject<Slider> = useRef<Slider>(null);
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.css');
SPComponentLoader.loadCss('https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick-theme.min.css');
let topElem: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
let _slider: React.MutableRefObject<Slider> = useRef<Slider>(null);
const [isSmall, isMedium] = useBreakpoints(props.clientWidth, [696, 928]);
let numSlides: number = 3;
@ -88,7 +86,7 @@ export const FilmstripLayout = (props: { children: any; clientWidth: number; the
</style>
}
<div>
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={props.ariaLabel} ref={ref}>
<div className={css(styles.filmstripLayout, styles.filmStrip)} aria-label={props.ariaLabel} ref={topElem}>
<Slider ref={_slider} {...settings}>
{props.children}
</Slider>

View File

@ -19,6 +19,16 @@ const sampleEvents: ICalendarEvent[] = [
location: "Barrie, ON",
description: "This is a description"
},
{
title: "This is a UTC event",
start: moment().add(1, "d").utc(false).toDate(),
end: moment().add(1, "d").add(1, "h").utc(false).toDate(),
url: "https://www.contoso.com/news-events/events/1UTC/",
allDay: false,
category: "Meeting",
location: "Barrie, ON",
description: "This is a description for a UTC event"
},
{
title: "This event will be in one week",
start: moment().add(1, "w").toDate(),