Added support re-sizing and semantic colors

This commit is contained in:
Hugo Bernier 2020-04-18 01:32:26 -04:00
parent 9647a8f920
commit 3b0022f214
10 changed files with 210 additions and 215 deletions

View File

@ -23,7 +23,7 @@
}
.cardWrapper .dateBox {
border-color: $ms-color-neutralTertiaryAlt;
border-color: $ms-color-neutralLight;
}
.normalCard .dateBoxContainer {
@ -44,6 +44,7 @@
.compactCard .dateBox {
margin: auto 14px auto 0;
min-width: 64px;
background-color: $ms-color-white;
}
[dir=rtl] .compactCard .dateBox {
@ -111,7 +112,7 @@
width: 100%;
align-items: center;
-webkit-font-smoothing: antialiased;
background-color: $ms-color-white;
//background-color: $ms-color-white;
border: 1px solid #eaeaea;
-webkit-box-sizing: border-box;
box-sizing: border-box;

View File

@ -8,168 +8,186 @@ 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';
/**
* Shows an event in a document card
*/
export class EventCard extends React.Component<IEventCardProps, {}> {
public render(): React.ReactElement<IEventCardProps> {
const { isNarrow } = this.props;
public render(): React.ReactElement<IEventCardProps> {
const { isNarrow } = this.props;
if (isNarrow) {
return this._renderNarrowCell();
} else {
return this._renderNormalCell();
}
if (isNarrow) {
return this._renderNarrowCell();
} else {
return this._renderNormalCell();
}
}
private _renderNormalCell(): JSX.Element {
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)}
data-is-focusable={true}
data-is-focus-item={true}
role="listitem"
aria-label={Text.format(strings.EventCardWrapperArialLabel, title, `${dateString}`)}
tabIndex={0}
private _renderNormalCell(): JSX.Element {
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)}
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}
>
<FocusZone>
<div className={styles.dateBoxContainer} style={{ height: 160 }}>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Medium}
/>
</div>
<div className={styles.detailsContainer}>
<div className={styles.category}>{category}</div>
<div className={styles.title}>{title}</div>
<div className={styles.datetime}>{dateString}</div>
<div className={styles.location}>{location}</div>
<ActionButton
className={styles.addToMyCalendar}
iconProps={{ iconName: "AddEvent" }}
ariaLabel={strings.AddToCalendarAriaLabel}
onClick={this._onAddToMyCalendar}
>
<DocumentCard
className={css(styles.root, !isEditMode && styles.rootIsActionable, styles.normalCard)}
type={DocumentCardType.normal}
onClickHref={isEditMode ? null : url}
>
<FocusZone>
<div className={styles.dateBoxContainer} style={{ height: 160 }}>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Medium}
/>
</div>
<div className={styles.detailsContainer}>
<div className={styles.category}>{category}</div>
<div className={styles.title}>{title}</div>
<div className={styles.datetime}>{dateString}</div>
<div className={styles.location}>{location}</div>
<ActionButton
className={styles.addToMyCalendar}
iconProps={{ iconName: "AddEvent" }}
ariaLabel={strings.AddToCalendarAriaLabel}
onClick={this._onAddToMyCalendar}
>
{strings.AddToCalendarButtonLabel}
</ActionButton>
</div>
</FocusZone>
</DocumentCard>
</div>
</div>
);
{strings.AddToCalendarButtonLabel}
</ActionButton>
</div>
</FocusZone>
</DocumentCard>
</div>
</div>
);
}
private _renderNarrowCell(): JSX.Element {
const { start,
end,
allDay,
title,
url,
// category,
// location
} = this.props.event;
const eventDate: moment.Moment = moment(start);
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
let customStyle: React.CSSProperties = {};
if (this.props.themeVariant) {
const { semanticColors }: IReadonlyTheme = this.props.themeVariant;
console.log("Semantic colors", semanticColors);
if (semanticColors && semanticColors.bodyBackground) {
customStyle = { backgroundColor: semanticColors.bodyBackground };
}
console.log("Custom style", semanticColors);
}
private _renderNarrowCell(): JSX.Element {
const { start,
end,
allDay,
title,
url,
// category,
// location
} = this.props.event;
const eventDate: moment.Moment = moment(start);
const dateString: string = allDay ? eventDate.format(strings.AllDayDateFormat) : eventDate.format(strings.LocalizedTimeFormat);
return (
return (
<div>
<div
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
style={customStyle}
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={customStyle}
onClickHref={url}
>
<div>
<div
className={css(styles.cardWrapper, styles.compactCard, styles.root, styles.rootIsCompact)}
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}
onClickHref={url}
>
<div>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Small}
/>
</div>
<div>
<div className={styles.title}>{title}</div>
<div className={styles.datetime}>{dateString}</div>
</div>
</DocumentCard>
</div>
<DateBox
className={styles.dateBox}
startDate={start}
endDate={end}
size={DateBoxSize.Small}
/>
</div>
);
<div>
<div className={styles.title}>{title}</div>
<div className={styles.datetime}>{dateString}</div>
</div>
</DocumentCard>
</div>
</div>
);
}
private _onAddToMyCalendar = (): void => {
const { event } = this.props;
// create a calendar to hold the event
const cal: ICS.VCALENDAR = new ICS.VCALENDAR();
cal.addProp("VERSION", 2.0);
cal.addProp("PRODID", "//SPFX//NONSGML v1.0//EN");
// create an event
const icsEvent: ICS.VEVENT = new ICS.VEVENT();
// generate a unique id
icsEvent.addProp("UID", Guid.newGuid().toString());
// if the event is all day, just pass the date component
if (event.allDay) {
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE" });
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE" });
} else {
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE-TIME" });
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE-TIME" });
icsEvent.addProp("DTEND", event.start, { VALUE: "DATE-TIME" });
}
private _onAddToMyCalendar = (): void => {
const { event } = this.props;
// add a title
icsEvent.addProp("SUMMARY", event.title);
// create a calendar to hold the event
const cal: ICS.VCALENDAR = new ICS.VCALENDAR();
cal.addProp("VERSION", 2.0);
cal.addProp("PRODID", "//SPFX//NONSGML v1.0//EN");
// create an event
const icsEvent: ICS.VEVENT = new ICS.VEVENT();
// generate a unique id
icsEvent.addProp("UID", Guid.newGuid().toString());
// if the event is all day, just pass the date component
if (event.allDay) {
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE" });
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE" });
} else {
icsEvent.addProp("DTSTAMP", event.start, { VALUE: "DATE-TIME" });
icsEvent.addProp("DTSTART", event.start, { VALUE: "DATE-TIME" });
icsEvent.addProp("DTEND", event.start, { VALUE: "DATE-TIME" });
}
// add a title
icsEvent.addProp("SUMMARY", event.title);
// add a url if there is one
if (event.url !== undefined) {
icsEvent.addProp("URL", event.url);
}
// add a description if there is one
if (event.description !== undefined) {
icsEvent.addProp("DESCRIPTION", event.description);
}
// add a location if there is one
if (event.location !== undefined) {
icsEvent.addProp("LOCATION", event.location);
}
// add the event to the calendar
cal.addComponent(icsEvent);
// export the calendar
// 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()));
// add a url if there is one
if (event.url !== undefined) {
icsEvent.addProp("URL", event.url);
}
// add a description if there is one
if (event.description !== undefined) {
icsEvent.addProp("DESCRIPTION", event.description);
}
// add a location if there is one
if (event.location !== undefined) {
icsEvent.addProp("LOCATION", event.location);
}
// add the event to the calendar
cal.addComponent(icsEvent);
// export the calendar
// 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()));
}
}

View File

@ -1,7 +1,9 @@
import { ICalendarEvent } from "../../services/CalendarService";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface IEventCardProps {
isEditMode: boolean;
event: ICalendarEvent;
isNarrow: boolean;
themeVariant?: IReadonlyTheme;
}

View File

@ -4,32 +4,7 @@ import * as React from 'react';
import Slider from 'react-slick';
import { SPComponentLoader } from '@microsoft/sp-loader';
import styles from "./FilmstripLayout.module.scss";
import { useRef, useEffect, useState } from 'react';
import useComponentSize from '@rehooks/component-size';
function useDebounce(value: number, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
import { useRef } from 'react';
function useBreakpoints(currentWidth: number, breakpoints: number[]) {
return breakpoints.map(breakpoint => currentWidth < breakpoint);
@ -39,39 +14,24 @@ function useBreakpoints(currentWidth: number, breakpoints: number[]) {
* Filmstrip layout
* Presents the child compoments as a slick slide
*/
export const FilmstripLayout = (props: { children: any; ariaLabel?: string; }) => {
export const FilmstripLayout = (props: { children: any; clientWidth: number; 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);
let size = useComponentSize(ref);
let { width } = size;
const debouncedWidth = useDebounce(width, 500);
const [numSlides, setNumSlides] = useState(3);
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');
useEffect(
() => {
console.log("New width", debouncedWidth);
// slick seems to have an issue with having "infinite" mode set to true and having less items than the number of slides per page
// set infinite to true only if there are more than 3 children
const [isSmall, isMedium] = useBreakpoints(debouncedWidth, [696, 928]);
const [isSmall, isMedium] = useBreakpoints(props.clientWidth, [696, 928]);
if (isSmall) {
setNumSlides(2);
} else if (isMedium) {
setNumSlides(3);
} else {
setNumSlides(4);
}
},
[debouncedWidth] // Only re-call effect if value or delay changes
);
let numSlides: number = 3;
if (isSmall) {
numSlides = 2;
} else if (isMedium) {
numSlides = 3;
} else {
numSlides = 4;
}
var isInfinite: boolean = React.Children.count(props.children) > numSlides;
var settings: any = {

View File

@ -0,0 +1,5 @@
export interface IFilmstripLayoutProps {
ariaLabel?: string;
clientWidth: number;
}

View File

@ -0,0 +1,4 @@
export interface IFilmstripLayoutState {
numSlides: number;
}

View File

@ -1 +1,2 @@
export * from "./FilmstripLayout";
export * from "./IFilmstripLayoutProps";

View File

@ -37,9 +37,6 @@ import { ICalendarFeedSummaryProps } from "./components/CalendarFeedSummary.type
// Support for theme variants
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme, ISemanticColors } from '@microsoft/sp-component-base';
// this is the same width that the SharePoint events web parts use to render as narrow
const MaxMobileWidth: number = 480;
/**
* Calendar Feed Summary Web Part
* This web part shows a summary of events, in a film-strip (for normal views) or list view (for narrow views)
@ -51,7 +48,7 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
private _clientWidth: number = undefined;
constructor() {
super();
@ -104,10 +101,10 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
* Renders the web part
*/
public render(): void {
const semanticColors: Readonly<ISemanticColors> | undefined = this._themeVariant && this._themeVariant.semanticColors;
// see if we need to render a mobile view
const isNarrow: boolean = this.domElement.clientWidth <= MaxMobileWidth;
if (this._clientWidth === undefined) {
this._clientWidth = this.domElement.clientWidth;
}
// display the summary (or the configuration screen)
const element: React.ReactElement<ICalendarFeedSummaryProps> = React.createElement(
@ -117,13 +114,13 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
displayMode: this.displayMode,
context: this.context,
isConfigured: this._isConfigured(),
isNarrow: isNarrow,
maxEvents: this.properties.maxEvents,
provider: this._getDataProvider(),
themeVariant: this._themeVariant,
updateProperty: (value: string) => {
this.properties.title = value;
},
clientWidth: this._clientWidth
}
);
@ -277,6 +274,7 @@ export default class CalendarFeedSummaryWebPart extends BaseClientSideWebPart<IC
* If we get resized, call the Render method so that we can switch between the narrow view and the regular view
*/
protected onAfterResize(newWidth: number): void {
this._clientWidth = newWidth;
// redraw the web part
this.render();
}

View File

@ -1,5 +1,4 @@
import { DisplayMode } from "@microsoft/sp-core-library";
import { SPComponentLoader } from "@microsoft/sp-loader";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import * as strings from "CalendarFeedSummaryWebPartStrings";
@ -17,6 +16,9 @@ import { IReadonlyTheme } from '@microsoft/sp-component-base';
// the key used when caching events
const CacheKey: string = "calendarFeedSummary";
// this is the same width that the SharePoint events web parts use to render as narrow
const MaxMobileWidth: number = 480;
/**
* Displays a feed summary from a given calendar feed provider. Renders a different view for mobile/narrow web parts.
*/
@ -108,7 +110,6 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
// put everything together in a nice little calendar view
return (
<div className={css(styles.calendarFeedSummary, styles.webPartChrome)} style={{backgroundColor: semanticColors.bodyBackground}}>
<div className={css(styles.webPartHeader, styles.headerSmMargin)}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
@ -126,8 +127,9 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
* Render your web part content
*/
private _renderContent(): JSX.Element {
const isNarrow: boolean = this.props.clientWidth < MaxMobileWidth;
const {
isNarrow,
displayMode
} = this.props;
const {
@ -257,11 +259,13 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
>
<List
items={pagedEvents}
onRenderCell={(item, index) => (
onRenderCell={(item, _index) => (
<EventCard
isEditMode={isEditMode}
event={item}
isNarrow={true} />
isNarrow={true}
themeVariant={this.props.themeVariant}
/>
)} />
{usePaging &&
<Paging
@ -293,6 +297,7 @@ export default class CalendarFeedSummary extends React.Component<ICalendarFeedSu
<div role="application">
<FilmstripLayout
ariaLabel={strings.FilmStripAriaLabel}
clientWidth={this.props.clientWidth}
>
{events.map((event: ICalendarEvent, index: number) => {
return (<EventCard

View File

@ -10,6 +10,7 @@ import { IWebPartContext } from "@microsoft/sp-webpart-base";
import { Moment } from "moment";
import { ICalendarEvent, ICalendarService } from "../../../shared/services/CalendarService";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
/**
* The props for the calendar feed summary component
*/
@ -19,10 +20,10 @@ export interface ICalendarFeedSummaryProps {
context: IWebPartContext;
updateProperty: (value: string) => void;
isConfigured: boolean;
isNarrow: boolean;
provider: ICalendarService;
maxEvents: number;
themeVariant: IReadonlyTheme;
clientWidth: number;
}
/**