properly stick 'New Activity' button on planner

fixes ADMIN-908

the 'new activity' indicator could end up in wrong place in
IE and Edge and "position: sticky" doesn't work either. Moved
the indicator rendering from PlannerApp to PlannerHeader so it
can use absolute positioning below and work everywhere.

test plan:
- Add global announcement to 'push down' planner header
- add new assignment or other activity like 4 weeks in past
- 'New Activity' button should stick like glue to below
  separator line of Dashboard header. no floating above it.

Change-Id: Ib89b4e6cd6a1f42a42f800c4bc0cb4cded4312a0
Reviewed-on: https://gerrit.instructure.com/149251
Tested-by: Jenkins
Reviewed-by: Ed Schiebel <eschiebel@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Carl Kibler 2018-05-04 11:34:01 -06:00 committed by Ed Schiebel
parent b0b9a38f7c
commit 6fbee3c841
10 changed files with 668 additions and 154 deletions

View File

@ -101,21 +101,7 @@ describe('PlannerApp', () => {
expect(mockUpdate).toHaveBeenCalledWith(0);
});
it('shows new activity button when new activity is indicated', () => {
const wrapper = shallow(<PlannerApp {...getDefaultValues()} firstNewActivityDate={moment('2017-03-30')} />);
expect(wrapper).toMatchSnapshot();
});
it('shows new activity button when there is new activity, but no current items', () => {
const wrapper = shallow(
<PlannerApp
days={[]}
timeZone="UTC"
changeToDashboardCardView={() => {}}
firstNewActivityDate={moment().add(-1, 'days')}
/>);
expect(wrapper.find('StickyButton')).toHaveLength(1);
});
it('shows load prior items button when there is more to load', () => {
const wrapper = shallow(<PlannerApp {...getDefaultValues()} />);

View File

@ -468,15 +468,6 @@ exports[`PlannerApp renders loading past spinner when loading past and there are
<div
className="PlannerApp large"
>
<StickyButton
buttonRef={[Function]}
direction="up"
hidden={true}
offset="0px"
onClick={[Function]}
>
New Activity
</StickyButton>
<View
as="div"
display="auto"
@ -503,87 +494,6 @@ exports[`PlannerApp renders loading past spinner when loading past and there are
</div>
`;
exports[`PlannerApp shows new activity button when new activity is indicated 1`] = `
<div
className="PlannerApp large"
>
<StickyButton
buttonRef={[Function]}
direction="up"
hidden={true}
offset="0px"
onClick={[Function]}
>
New Activity
</StickyButton>
<View
as="div"
display="auto"
textAlign="center"
>
<ShowOnFocusButton
buttonProps={
Object {
"onClick": undefined,
}
}
buttonRef={[Function]}
>
Load prior dates
</ShowOnFocusButton>
</View>
<LoadingPastIndicator
allPastItemsLoaded={false}
loadingPast={false}
/>
<Animatable(Day)
animatableIndex={1}
day="2017-04-24"
itemsForDay={
Array [
Object {
"dateBucketMoment": "2017-04-23T15:00:00.000Z",
},
]
}
timeZone="Asia/Tokyo"
/>
<Animatable(Day)
animatableIndex={2}
day="2017-04-25"
itemsForDay={
Array [
Object {
"dateBucketMoment": "2017-04-24T15:00:00.000Z",
},
]
}
timeZone="Asia/Tokyo"
/>
<Animatable(Day)
animatableIndex={3}
day="2017-04-26"
itemsForDay={
Array [
Object {
"dateBucketMoment": "2017-04-25T15:00:00.000Z",
},
]
}
timeZone="Asia/Tokyo"
/>
<div
id="planner-app-fixed-element"
/>
<LoadingFutureIndicator
allFutureItemsLoaded={false}
loadingFuture={false}
onLoadMore={[Function]}
plannerActive={[Function]}
/>
</div>
`;
exports[`PlannerApp shows only the loading component when the isLoading prop is true 1`] = `
<div
className="PlannerApp large"

View File

@ -28,13 +28,11 @@ import { userShape, sizeShape } from '../plannerPropTypes';
import Day from '../Day';
import EmptyDays from '../EmptyDays';
import ShowOnFocusButton from '../ShowOnFocusButton';
import StickyButton from '../StickyButton';
import LoadingFutureIndicator from '../LoadingFutureIndicator';
import LoadingPastIndicator from '../LoadingPastIndicator';
import PlannerEmptyState from '../PlannerEmptyState';
import formatMessage from '../../format-message';
import {loadFutureItems, loadPastButtonClicked, loadPastUntilNewActivity, scrollToNewActivity, togglePlannerItemCompletion, updateTodo} from '../../actions';
import {getFirstLoadedMoment} from '../../utilities/dateUtils';
import {loadFutureItems, loadPastButtonClicked, loadPastUntilNewActivity, togglePlannerItemCompletion, updateTodo} from '../../actions';
import {notifier} from '../../dynamic-ui';
import {daysToDaysHash} from '../../utilities/daysUtils';
import {formatDayKey} from '../../utilities/dateUtils';
@ -55,13 +53,10 @@ export class PlannerApp extends Component {
allPastItemsLoaded: bool,
loadingFuture: bool,
allFutureItemsLoaded: bool,
firstNewActivityDate: momentObj,
loadPastButtonClicked: func,
loadPastUntilNewActivity: func,
scrollToNewActivity: func,
loadFutureItems: func,
stickyOffset: number, // in pixels
stickyZIndex: number,
changeToDashboardCardView: func,
togglePlannerItemCompletion: func,
updateTodo: func,
@ -129,10 +124,6 @@ export class PlannerApp extends Component {
this.fixedElement = elt;
}
handleNewActivityClick = () => {
this.props.scrollToNewActivity();
}
// when the planner changes layout, its contents move and the user gets lost.
// let's help with that.
@ -181,28 +172,6 @@ export class PlannerApp extends Component {
</View>;
}
renderNewActivity () {
if (this.props.isLoading) return;
if (!this.props.firstNewActivityDate) return;
const firstLoadedMoment = getFirstLoadedMoment(this.props.days, this.props.timeZone);
const firstNewActivityLoaded = firstLoadedMoment.isSame(this.props.firstNewActivityDate) || firstLoadedMoment.isBefore(this.props.firstNewActivityDate);
if (firstNewActivityLoaded && !this.props.ui.naiAboveScreen) return;
return (
<StickyButton
direction="up"
hidden={true}
onClick={this.handleNewActivityClick}
offset={this.props.stickyOffset + 'px'}
zIndex={this.props.stickyZIndex}
buttonRef={ref => this.newActivityButtonRef = ref}
>
{formatMessage("New Activity")}
</StickyButton>
);
}
renderLoadingPast () {
return <LoadingPastIndicator
loadingPast={this.props.loadingPast}
@ -398,14 +367,12 @@ export class PlannerApp extends Component {
const loading = this.props.loadingPast || this.props.loadingFuture || this.props.isLoading;
if (children.length === 0 && !loading) {
return <div className={classes}>
{this.renderNewActivity()}
{this.renderLoadPastButton()}
{this.renderNoAssignments()}
</div>;
}
return <div className={classes} ref={el => this._plannerElem = el}>
{this.renderNewActivity()}
{this.renderLoadPastButton()}
{this.renderLoadingPast()}
{children}
@ -435,12 +402,11 @@ const mapStateToProps = (state) => {
loadingFuture: state.loading.loadingFuture,
allFutureItemsLoaded: state.loading.allFutureItemsLoaded,
loadingError: state.loading.loadingError,
firstNewActivityDate: state.firstNewActivityDate,
timeZone: state.timeZone,
ui: state.ui,
};
};
const ResponsivePlannerApp = responsiviser()(PlannerApp);
const mapDispatchToProps = {loadFutureItems, loadPastButtonClicked, loadPastUntilNewActivity, scrollToNewActivity, togglePlannerItemCompletion, updateTodo};
const mapDispatchToProps = {loadFutureItems, loadPastButtonClicked, loadPastUntilNewActivity, togglePlannerItemCompletion, updateTodo};
export default notifier(connect(mapStateToProps, mapDispatchToProps)(ResponsivePlannerApp));

View File

@ -18,7 +18,16 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { PlannerHeader } from '../index';
import moment from "moment-timezone";
import sinon from 'sinon';
import {getFirstLoadedMoment} from "../../../utilities/dateUtils";
const TZ = 'America/Denver';
const plannerDays = [
moment("2018-03-01T14:00:42Z").tz(TZ),
moment("2018-03-02T14:00:42Z").tz(TZ),
moment("2018-03-03T14:00:42Z").tz(TZ)
];
function defaultProps (options) {
return {
@ -27,17 +36,21 @@ function defaultProps (options) {
items: [{id: "1", course_id: "1", due_at: "2017-03-09T20:40:35Z", html_url: "http://www.non_default_url.com", name: "learning object title"}],
nextUrl: null
},
days: plannerDays.map(d => [d.format('YYYY-MM-DD'), [{dateBucketMoment: d}]]),
getInitialOpportunities: () => {},
getNextOpportunities: () => {},
savePlannerItem: () => {},
locale: 'en',
timeZone: 'America/Denver',
timeZone: TZ,
deletePlannerItem: () => {},
dismissOpportunity: () => {},
clearUpdateTodo: () => {},
startLoadingGradesSaga: () => {},
ariaHideElement: document.createElement('div'),
stickyZIndex: 3,
firstNewActivityDate: null,
loading: {
isLoading: false,
allPastItemsLoaded: false,
allFutureItemsLoaded: false,
allOpportunitiesLoaded: false,
@ -49,6 +62,9 @@ function defaultProps (options) {
loadingGrades: false,
gradesLoaded: false,
},
ui: {
naiAboveScreen: false
},
todo: {
updateTodoItem: null
},
@ -172,6 +188,7 @@ it('does not call getNextOpportunities when component has 12 opportunities', ()
];
props.loading = {
isLoading: false,
allPastItemsLoaded: false,
allFutureItemsLoaded: false,
allOpportunitiesLoaded: false,
@ -402,3 +419,95 @@ it('does not start the grades saga when grades have been loaded', () => {
wrapper.instance().toggleGradesTray();
expect(props.startLoadingGradesSaga).not.toHaveBeenCalled();
});
describe('new activity button', () => {
let spy;
beforeEach(() => {
spy = sinon.stub(PlannerHeader.prototype, 'newActivityAboveView');
});
afterEach(() => {
spy.reset();
spy.restore();
});
it('does not show when there is no new activity', () => {
spy.returns(false);
const wrapper = shallow(<PlannerHeader {...defaultProps()} />);
expect(wrapper).toMatchSnapshot();
expect(spy.calledOnce).toEqual(true);
});
it('shows when there is new activity', () => {
spy.returns(true);
const wrapper = shallow(<PlannerHeader {...defaultProps()} />);
expect(wrapper).toMatchSnapshot();
expect(spy.calledOnce).toEqual(true);
});
});
describe('decision to show new activity indicator', () => {
it('is false while data is loading', () => {
const props = defaultProps();
props.loading.isLoading = true;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(false);
});
it('is false when there are no planner items', () => {
const props = defaultProps();
props.days = [];
props.loading.isLoading = false;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(false);
});
it('is false when first activity date is unknown', () => {
const props = defaultProps();
props.loading.isLoading = false;
props.firstNewActivityDate = undefined;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(false);
});
it('is false when the newest activity is already on or below the viewport', () => {
const props = defaultProps();
props.loading.isLoading = false;
props.firstNewActivityDate = plannerDays[0];
props.ui.naiAboveScreen = false;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(false);
});
it('is true when there is new activity still to be loaded from the past', () => {
const props = defaultProps();
props.loading.isLoading = false;
props.firstNewActivityDate = moment(plannerDays[0]);
props.firstNewActivityDate.subtract(5, 'days');
props.ui.naiAboveScreen = false;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(true);
});
it('is true when a new activity is above the viewport', () => {
const props = defaultProps();
props.loading.isLoading = false;
props.firstNewActivityDate = plannerDays[0];
props.ui.naiAboveScreen = true;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(true);
});
it('is true when there is new activity but no current items', () => {
const props = defaultProps();
props.days = [];
props.loading.isLoading = false;
props.firstNewActivityDate = plannerDays[0];
props.ui.naiAboveScreen = true;
const wrapper = shallow(<PlannerHeader {...props} />);
expect(wrapper.instance().newActivityAboveView()).toEqual(true);
});
});

View File

@ -1,5 +1,496 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`new activity button does not show when there is no new activity 1`] = `
<div>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="light"
>
Today
</Button>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<IconPlusLine
title="Add To Do"
/>
</Button>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<IconGradebookLine
title="Show My Grades"
/>
</Button>
<Popover
alignArrow={false}
applicationElement={null}
closeButtonLabel={null}
closeButtonRef={[Function]}
constrain="none"
contentRef={[Function]}
defaultFocusElement={null}
defaultShow={false}
insertAt="bottom"
label={null}
mountNode={null}
offsetX={0}
offsetY={0}
on="click"
onBlur={[Function]}
onClick={[Function]}
onDismiss={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onPositionChanged={[Function]}
onPositioned={[Function]}
onShow={[Function]}
onToggle={[Function]}
placement="bottom end"
positionTarget={null}
shouldCloseOnDocumentClick={true}
shouldCloseOnEscape={true}
shouldContainFocus={false}
shouldRenderOffscreen={false}
shouldReturnFocus={true}
show={false}
trackPosition={true}
variant="default"
withArrow={true}
>
<PopoverTrigger>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<Badge
count={1}
formatOverflowText={[Function]}
placement="top end"
pulse={false}
standalone={false}
type="count"
variant="primary"
>
<IconAlertsLine
title="1 opportunity"
/>
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent>
<Animatable(Opportunities)
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
dismiss={[Function]}
maxHeight="none"
opportunities={
Array [
Object {
"course_id": "1",
"due_at": "2017-03-09T20:40:35Z",
"html_url": "http://www.non_default_url.com",
"id": "1",
"name": "learning object title",
},
]
}
timeZone="America/Denver"
togglePopover={[Function]}
/>
</PopoverContent>
</Popover>
<Tray
applicationElement={[Function]}
border={false}
closeButtonLabel="Close"
closeButtonRef={[Function]}
closeButtonVariant="icon"
contentRef={[Function]}
defaultFocusElement={null}
insertAt="bottom"
label="Add To Do"
mountNode={null}
onClose={[Function]}
onDismiss={[Function]}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
onOpen={[Function]}
open={false}
placement="end"
shadow={true}
shouldCloseOnDocumentClick={false}
shouldContainFocus={true}
shouldReturnFocus={false}
size="small"
>
<UpdateItemTray
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
locale="en"
noteItem={null}
onDeletePlannerItem={[Function]}
onSavePlannerItem={[Function]}
timeZone="America/Denver"
/>
</Tray>
<Tray
applicationElement={[Function]}
border={false}
closeButtonLabel={null}
closeButtonRef={[Function]}
closeButtonVariant="icon"
contentRef={[Function]}
defaultFocusElement={null}
insertAt="bottom"
label="My Grades"
mountNode={null}
onClose={[Function]}
onDismiss={[Function]}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
onOpen={[Function]}
open={false}
placement="end"
shadow={true}
shouldCloseOnDocumentClick={false}
shouldContainFocus={true}
shouldReturnFocus={true}
size="small"
>
<View
as="div"
display="auto"
padding="large large medium"
>
<CloseButton
buttonRef={[Function]}
margin="0"
offset="x-small"
onClick={[Function]}
placement="start"
size="small"
variant="icon"
>
Close
</CloseButton>
<GradesDisplay
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
loading={false}
/>
</View>
</Tray>
</div>
`;
exports[`new activity button shows when there is new activity 1`] = `
<div>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="light"
>
Today
</Button>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<IconPlusLine
title="Add To Do"
/>
</Button>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<IconGradebookLine
title="Show My Grades"
/>
</Button>
<Popover
alignArrow={false}
applicationElement={null}
closeButtonLabel={null}
closeButtonRef={[Function]}
constrain="none"
contentRef={[Function]}
defaultFocusElement={null}
defaultShow={false}
insertAt="bottom"
label={null}
mountNode={null}
offsetX={0}
offsetY={0}
on="click"
onBlur={[Function]}
onClick={[Function]}
onDismiss={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onPositionChanged={[Function]}
onPositioned={[Function]}
onShow={[Function]}
onToggle={[Function]}
placement="bottom end"
positionTarget={null}
shouldCloseOnDocumentClick={true}
shouldCloseOnEscape={true}
shouldContainFocus={false}
shouldRenderOffscreen={false}
shouldReturnFocus={true}
show={false}
trackPosition={true}
variant="default"
withArrow={true}
>
<PopoverTrigger>
<Button
as="button"
buttonRef={[Function]}
fluidWidth={false}
margin="0 medium 0 0"
onClick={[Function]}
size="medium"
type="button"
variant="icon"
>
<Badge
count={1}
formatOverflowText={[Function]}
placement="top end"
pulse={false}
standalone={false}
type="count"
variant="primary"
>
<IconAlertsLine
title="1 opportunity"
/>
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent>
<Animatable(Opportunities)
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
dismiss={[Function]}
maxHeight="none"
opportunities={
Array [
Object {
"course_id": "1",
"due_at": "2017-03-09T20:40:35Z",
"html_url": "http://www.non_default_url.com",
"id": "1",
"name": "learning object title",
},
]
}
timeZone="America/Denver"
togglePopover={[Function]}
/>
</PopoverContent>
</Popover>
<Tray
applicationElement={[Function]}
border={false}
closeButtonLabel="Close"
closeButtonRef={[Function]}
closeButtonVariant="icon"
contentRef={[Function]}
defaultFocusElement={null}
insertAt="bottom"
label="Add To Do"
mountNode={null}
onClose={[Function]}
onDismiss={[Function]}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
onOpen={[Function]}
open={false}
placement="end"
shadow={true}
shouldCloseOnDocumentClick={false}
shouldContainFocus={true}
shouldReturnFocus={false}
size="small"
>
<UpdateItemTray
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
locale="en"
noteItem={null}
onDeletePlannerItem={[Function]}
onSavePlannerItem={[Function]}
timeZone="America/Denver"
/>
</Tray>
<Tray
applicationElement={[Function]}
border={false}
closeButtonLabel={null}
closeButtonRef={[Function]}
closeButtonVariant="icon"
contentRef={[Function]}
defaultFocusElement={null}
insertAt="bottom"
label="My Grades"
mountNode={null}
onClose={[Function]}
onDismiss={[Function]}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
onOpen={[Function]}
open={false}
placement="end"
shadow={true}
shouldCloseOnDocumentClick={false}
shouldContainFocus={true}
shouldReturnFocus={true}
size="small"
>
<View
as="div"
display="auto"
padding="large large medium"
>
<CloseButton
buttonRef={[Function]}
margin="0"
offset="x-small"
onClick={[Function]}
placement="start"
size="small"
variant="icon"
>
Close
</CloseButton>
<GradesDisplay
courses={
Array [
Object {
"id": "1",
"informStudentsOfOverdueSubmissions": true,
"shortName": "Course Short Name",
},
]
}
loading={false}
/>
</View>
</Tray>
<StickyButton
buttonRef={[Function]}
className="StickyButton-styles__newActivityButton"
direction="up"
hidden={true}
offset="0"
onClick={[Function]}
zIndex={3}
>
New Activity
</StickyButton>
</div>
`;
exports[`renders the base component correctly with buttons and trays 1`] = `
<div>
<Button

View File

@ -31,9 +31,12 @@ import Tray from '@instructure/ui-core/lib/components/Tray';
import Badge from '@instructure/ui-core/lib/components/Badge';
import Opportunities from '../Opportunities';
import GradesDisplay from '../GradesDisplay';
import StickyButton from '../StickyButton';
import {
addDay, savePlannerItem, deletePlannerItem, cancelEditingPlannerItem, openEditingPlannerItem, getNextOpportunities,
getInitialOpportunities, dismissOpportunity, clearUpdateTodo, startLoadingGradesSaga, scrollToToday
getInitialOpportunities, dismissOpportunity, clearUpdateTodo, startLoadingGradesSaga, scrollToToday,
scrollToNewActivity
} from '../../actions';
import { courseShape, opportunityShape } from '../plannerPropTypes';
@ -41,6 +44,8 @@ import styles from './styles.css';
import theme from './theme.js';
import formatMessage from '../../format-message';
import {notifier} from '../../dynamic-ui';
import {getFirstLoadedMoment} from "../../utilities/dateUtils";
import {momentObj} from "react-moment-proptypes";
export class PlannerHeader extends Component {
@ -54,6 +59,7 @@ export class PlannerHeader extends Component {
triggerDynamicUiUpdates: PropTypes.func,
preTriggerDynamicUiUpdates: PropTypes.func,
scrollToToday: PropTypes.func,
scrollToNewActivity: PropTypes.func,
locale: PropTypes.string.isRequired,
timeZone: PropTypes.string.isRequired,
opportunities: PropTypes.shape(opportunityShape).isRequired,
@ -62,12 +68,23 @@ export class PlannerHeader extends Component {
dismissOpportunity: PropTypes.func.isRequired,
clearUpdateTodo: PropTypes.func.isRequired,
startLoadingGradesSaga: PropTypes.func.isRequired,
firstNewActivityDate: momentObj,
days: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.oneOfType([/* date */ PropTypes.string, PropTypes.arrayOf(/* items */ PropTypes.object)])
)
),
ui: PropTypes.shape({
naiAboveScreen: PropTypes.bool,
}),
todo: PropTypes.shape({
updateTodoItem: PropTypes.shape({
title: PropTypes.string,
}),
}),
stickyZIndex: PropTypes.number,
loading: PropTypes.shape({
isLoading: PropTypes.bool,
allPastItemsLoaded: PropTypes.bool,
allFutureItemsLoaded: PropTypes.bool,
allOpportunitiesLoaded: PropTypes.bool,
@ -87,6 +104,7 @@ export class PlannerHeader extends Component {
static defaultProps = {
triggerDynamicUiUpdates: () => {},
preTriggerDynamicUiUpdates: () => {},
stickyZIndex: 0
}
constructor (props) {
@ -190,6 +208,10 @@ export class PlannerHeader extends Component {
}
}
handleNewActivityClick = () => {
this.props.scrollToNewActivity();
}
_doToggleOpportunitiesDropdown (openOrClosed) {
this.setState({opportunitiesOpen: !!openOrClosed}, () => {
this.toggleAriaHiddenStuff(this.state.opportunitiesOpen);
@ -244,6 +266,32 @@ export class PlannerHeader extends Component {
return 'none';
}
newActivityAboveView () {
if (this.props.loading.isLoading) return false;
if (!this.props.firstNewActivityDate) return false;
const firstLoadedMoment = getFirstLoadedMoment(this.props.days, this.props.timeZone);
const firstNewActivityLoaded = firstLoadedMoment.isSame(this.props.firstNewActivityDate) || firstLoadedMoment.isBefore(this.props.firstNewActivityDate);
return (!(firstNewActivityLoaded && !this.props.ui.naiAboveScreen))
}
renderNewActivity () {
if (this.newActivityAboveView()) {
return (
<StickyButton
direction="up"
hidden={true}
onClick={this.handleNewActivityClick}
zIndex={this.props.stickyZIndex}
buttonRef={ref => this.newActivityButtonRef = ref}
className="StickyButton-styles__newActivityButton"
>
{formatMessage("New Activity")}
</StickyButton>
)
}
}
render () {
const verticalRoom = this.getPopupVerticalRoom();
@ -342,6 +390,7 @@ export class PlannerHeader extends Component {
/>
</View>
</Tray>
{this.renderNewActivity()}
</div>
);
}
@ -350,10 +399,12 @@ export class PlannerHeader extends Component {
export const ThemedPlannerHeader = themeable(theme, styles)(PlannerHeader);
export const NotifierPlannerHeader = notifier(ThemedPlannerHeader);
const mapStateToProps = ({opportunities, loading, courses, todo}) => ({opportunities, loading, courses, todo});
const mapStateToProps = ({opportunities, loading, courses, todo, days, timeZone, ui, firstNewActivityDate}) =>
({opportunities, loading, courses, todo, days, timeZone, ui, firstNewActivityDate});
const mapDispatchToProps = {
addDay, savePlannerItem, deletePlannerItem, cancelEditingPlannerItem, openEditingPlannerItem,
getInitialOpportunities, getNextOpportunities, dismissOpportunity, clearUpdateTodo, startLoadingGradesSaga, scrollToToday
getInitialOpportunities, getNextOpportunities, dismissOpportunity, clearUpdateTodo,
startLoadingGradesSaga, scrollToToday, scrollToNewActivity
};
export default connect(mapStateToProps, mapDispatchToProps)(NotifierPlannerHeader);

View File

@ -8,7 +8,6 @@ exports[`adds aria-hidden when specified 1`] = `
onClick={[Function]}
style={
Object {
"top": "0",
"zIndex": null,
}
}
@ -28,7 +27,6 @@ exports[`renders 1`] = `
onClick={[Function]}
style={
Object {
"top": "0",
"zIndex": null,
}
}

View File

@ -32,14 +32,15 @@ class StickyButton extends Component {
disabled: bool,
hidden: bool,
direction: oneOf(['none', 'up', 'down']),
className: string,
zIndex: number,
offset: string,
buttonRef: func,
};
static defaultProps = {
direction: 'none',
offset: '0',
className: ''
};
handleClick = (e) => {
@ -75,7 +76,6 @@ class StickyButton extends Component {
hidden,
direction,
zIndex,
offset,
} = this.props;
const classes = {
@ -85,14 +85,13 @@ class StickyButton extends Component {
const style = {
zIndex: (zIndex) ? zIndex : null,
top: offset,
};
return (
<button
type="button"
onClick={this.handleClick}
className={classnames(classes)}
className={classnames(classes, styles.newActivityButton)}
style={style}
aria-disabled={(disabled) ? 'true' : null}
aria-hidden={(hidden) ? 'true' : null}

View File

@ -85,3 +85,9 @@
height: 100%;
padding: var(--padding);
}
.newActivityButton {
offset-inline-start: 0;
top: 100%;
position: absolute;
}

View File

@ -99,7 +99,6 @@ export function render (element, options) {
<PlannerApp
appRef={app => dynamicUiManager.setApp(app)}
stickyOffset={opts.stickyOffset}
stickyZIndex={opts.stickyZIndex}
changeToDashboardCardView={opts.changeToDashboardCardView}
plannerActive={plannerActive}
currentUser={opts.currentUser}
@ -118,6 +117,7 @@ export function renderHeader (element, options) {
<DynamicUiProvider manager={dynamicUiManager} >
<Provider store={store}>
<PlannerHeader
stickyZIndex={opts.stickyZIndex}
timeZone={opts.timeZone}
locale={opts.locale}
ariaHideElement={opts.ariaHideElement}
@ -154,7 +154,7 @@ export default function loadPlannerDashboard ({changeToCardView, getActiveApp, f
})) : [];
const stickyElementRect = stickyElement.getBoundingClientRect();
const stickyOffset = stickyElementRect.bottom - stickyElementRect.top;
const stickyOffset = stickyElementRect.bottom - stickyElementRect.top + 24;
plannerActive = () => getActiveApp() === 'planner';
const options = {
@ -173,8 +173,6 @@ export default function loadPlannerDashboard ({changeToCardView, getActiveApp, f
},
ariaHideElement: document.getElementById('application'),
theme: '',
// the new activity button isn't sticky in IE yet, so make sure it slides
// under the header that is sticky in IE
stickyZIndex: 3,
stickyOffset: stickyOffset,
courses: courses,