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:
parent
b0b9a38f7c
commit
6fbee3c841
|
@ -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()} />);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -85,3 +85,9 @@
|
|||
height: 100%;
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
.newActivityButton {
|
||||
offset-inline-start: 0;
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue