Implement responsive dashboard

Replace container query css with Responsive component

This commit changes planner from using the instui containerQuery based
data-media-* attributes to the new Responsive component. When it's
complete, the wide-screen planner should look just like it did before,
though all the responsive changes have been removed in prep for building
the new designs in upcoming commits.

While updating the specs, I also cleaned up a few xpath
contains-text expressions.

closes ADMIN-871

test plan:
  - The planner should look just like it did before in normal desktop
    view.

Change-Id: I39cd99b6a29eb01bca25392f77ca96acd3d56c7e

Implement responsive dashboard

also refactored the Indicators into their own component directory

fixes ADMIN-872

test plan:
  - load the planner
  - shrink your browser's width
  > see that it reconfigures itsef to look like the tablet design
  - click the New Activity button
  > ensure that it scrolls to the next previous new activity

Change-Id: I73966fe1b1eefa69d0f0df404bb6da6d89a25471
Reviewed-on: https://gerrit.instructure.com/144279
Tested-by: Jenkins
Reviewed-by: Jon Willesen <jonw+gerrit@instructure.com>
QA-Review: Jon Willesen <jonw+gerrit@instructure.com>
Product-Review: Mary Jane Anderson <manderson@instructure.com>
This commit is contained in:
Ed Schiebel 2018-03-16 11:50:45 -04:00
parent 10c390a2ac
commit f7be7df9a6
47 changed files with 1796 additions and 1055 deletions

View File

@ -26,9 +26,12 @@ body {
background: $ic-color-light;
font-weight: 300;
&:not(.is-inside-submission-frame):not(.embedded) {
&:not(.is-inside-submission-frame):not(.embedded):not(.dashboard-is-planner) {
min-width: 768px;
}
&.dashboard-is-planner {
min-width: 470px; /* or the dashboard header starts to wrap */
}
&.no-headers, &.embedded {
#header, #topbar, #left-side, #breadcrumbs { display: none !important; }

View File

@ -34,6 +34,10 @@ Finally, start watched builds
Now any changes to the planner source will trigger a planner incremental build, which will in turn trigger
a canvas incremental build.
If you are doing a lot of CSS work, the watch commands don't track changes so well. If you find this is the case,
you can run `yarn build:dev`. This variant does not watch, but still sets up the environment so that class
names and theme variables are not mangled by the INSTUI themeable tooling.
> *Any commands discussed in the rest of this document assume your current working directory is `canvas-lms/packages/canvas-planner`.*
### Linting

View File

@ -20,7 +20,7 @@ if (env === 'test') {
module.exports = {
// eslint-disable-next-line import/no-extraneous-dependencies
presets: [[ require('@instructure/ui-presets/babel'), {
themeable: process.env.BABEL_ENV === 'production',
themeable: true,
coverage: false,
esModules: Boolean(process.env.ES_MODULES)
}]]

View File

@ -1,13 +1,13 @@
/*
* Copyright (C) <%= YEAR %> - present Instructure, Inc.
*
* This module is part of Canvas.
* This file is part of Canvas.
*
* This module and Canvas are free software: you can redistribute them and/or modify them under
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* This module and Canvas are distributed in the hope that they will be useful, but WITHOUT ANY
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
module.exports = {
module.exports = {
transform: {
'^.+\\.(js)$': 'babel-jest',
'^.+\\.(css)$': '<rootDir>/jest-themeable-styles'

View File

@ -15,9 +15,10 @@
"lint:fix": "eslint src/ --fix",
"build": "./scripts/build",
"prepublish": "yarn build",
"build:dev": "NODE_ENV=development BABEL_ENV=production babel src --out-dir lib --ignore spec.js,test.js,demo.js --quiet",
"build:lib": "BABEL_ENV=production babel src --out-dir lib --ignore spec.js,test.js,demo.js --quiet",
"build:es": "BABEL_ENV=production ES_MODULES=1 babel src --out-dir es --ignore spec.js,test.js,demo.js --quiet",
"build:watch": "yarn run build:lib -- --watch",
"build:watch": "yarn run build:dev --watch",
"test": "jest",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
@ -68,7 +69,7 @@
"velocity-animate": "^1.5.0"
},
"devDependencies": {
"@instructure/ui-presets": "^4.6.0",
"@instructure/ui-presets": "^4.7.2",
"babel-cli": "^6",
"babel-core": "^6",
"babel-eslint": "^7",

View File

@ -76,3 +76,15 @@ it('registers itself as animatable', () => {
wrapper.unmount();
expect(fakeDeregister).toHaveBeenCalledWith('item', instance, ['2', '3', '4']);
});
it('renders its own NotificationBadge when asked to', () => {
const wrapper = mount(
<CompletedItemsFacade
onClick={() => {}}
notificationBadge="newActivity"
itemCount={3}
animatableItemIds={['1', '2', '3']}
/>
);
expect(wrapper.find('NewActivityIndicator')).toHaveLength(1);
});

View File

@ -2,6 +2,7 @@
exports[`renders as a div with a Checkbox and a string of text indicating count 1`] = `
<div>
<NotificationBadge />
<div>
<Button
as="button"

View File

@ -17,12 +17,12 @@
*/
import React, { Component } from 'react';
import themeable from '@instructure/ui-themeable/lib';
import containerQuery from '@instructure/ui-utils/lib/react/containerQuery';
import Button from '@instructure/ui-core/lib/components/Button';
import Pill from '@instructure/ui-core/lib/components/Pill';
import IconArrowOpenRight from 'instructure-icons/lib/Solid/IconArrowOpenRightSolid';
import BadgeList from '../BadgeList';
import { func, number, string, arrayOf, shape } from 'prop-types';
import NotificationBadge, { MissingIndicator, NewActivityIndicator} from '../NotificationBadge';
import { func, number, string, arrayOf, shape, oneOf } from 'prop-types';
import { badgeShape } from '../plannerPropTypes';
import {animatable} from '../../dynamic-ui';
@ -32,7 +32,6 @@ import theme from './theme.js';
import formatMessage from '../../format-message';
export class CompletedItemsFacade extends Component {
static propTypes = {
onClick: func.isRequired,
itemCount: number.isRequired,
@ -41,13 +40,14 @@ export class CompletedItemsFacade extends Component {
animatableItemIds: arrayOf(string),
registerAnimatable: func,
deregisterAnimatable: func,
}
notificationBadge: oneOf(['none', 'newActivity', 'missing']),
};
static defaultProps = {
badges: [],
registerAnimatable: () => {},
deregisterAnimatable: () => {},
}
notificationBadge: 'none',
};
componentDidMount () {
this.props.registerAnimatable('item', this, this.props.animatableIndex, this.props.animatableItemIds);
@ -84,9 +84,25 @@ export class CompletedItemsFacade extends Component {
return null;
}
renderNotificationBadge () {
if (this.props.notificationBadge === 'none') return null;
const isNewItem = this.props.notificationBadge === 'newActivity';
const IndicatorComponent = isNewItem ? NewActivityIndicator : MissingIndicator;
const badgeMessage = formatMessage('{items} completed {items, plural,=1 {item} other {items}}', {items: this.props.itemCount});
return (
<div className={styles.activityIndicator}>
<IndicatorComponent
title={badgeMessage}
itemIds={this.props.animatableItemIds}
animatableIndex={this.props.animatableIndex} />
</div>
);
}
render () {
return (
<div className={styles.root} ref={elt => this.rootDiv = elt}>
<NotificationBadge>{this.renderNotificationBadge()}</NotificationBadge>
<div className={styles.contentPrimary}>
<Button
variant="link"
@ -114,11 +130,4 @@ export class CompletedItemsFacade extends Component {
}
}
export default animatable(themeable(theme, styles)(
// we can update this to be whatever works for this component and its content
containerQuery({
'media-x-large': { minWidth: '68rem' },
'media-large': { minWidth: '58rem' },
'media-medium': { minWidth: '34rem' }
})(CompletedItemsFacade)
));
export default animatable(themeable(theme, styles)(CompletedItemsFacade));

View File

@ -1,4 +1,7 @@
.root {
display: flex;
flex:1;
align-items: center;
font-family: var(--fontFamily);
color: var(--color);
box-sizing: border-box;
@ -6,40 +9,30 @@
border-bottom: var(--borderWidth) solid var(--borderColor);
}
.activityIndicator {
padding-right: 0;
padding-left: 0;
}
.showLabel {
margin-left: var(--gutterWidth);
}
.contentPrimary {
margin-bottom: var(--bottomMarginPhoneUp);
flex: 0 0 50%;
margin-bottom: 0;
margin-left: calc(var(--gutterWidth) - var(--buttonPadding)); /* account for the padding inside the button */
box-sizing: border-box;
min-width: 1px;
}
.contentSecondary {
margin-left: var(--gutterWidth);
flex: 0 0 50%;
box-sizing: border-box;
min-width: 1px;
text-align: end;
}
[data-media-medium] {
&.root {
display: flex;
align-items: center;
}
.contentPrimary {
margin-bottom: 0;
flex: 1;
}
.contentSecondary {
margin-left: 1rem;
}
}
[data-media-x-large] {
&.root {
}
.activityIndicator + .contentPrimary {
margin-left: calc(var(--gutterWidth) - var(--buttonPadding) - var(--activityIndicatorWidth));
}

View File

@ -17,7 +17,7 @@
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export default function generator ({ borders, colors, media, spacing, typography }) {
export default function generator ({ borders, colors, spacing, typography }) {
return {
fontFamily: typography.fontFamily,
color: colors.licorice,
@ -32,7 +32,5 @@ export default function generator ({ borders, colors, media, spacing, typography
gutterWidth: spacing.medium,
buttonPadding: spacing.small,
...media
};
}

View File

@ -27,7 +27,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
</Text>
</Heading>
<div>
<Animatable(Grouping)
<Animatable(ResponsiveComponent)
animatableIndex={0}
items={
Array [
@ -50,7 +50,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
timeZone="America/Denver"
url="http://www.non_default_url.com"
/>
<Animatable(Grouping)
<Animatable(ResponsiveComponent)
animatableIndex={1}
items={
Array [
@ -82,7 +82,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
timeZone="America/Denver"
url="http://www.non_default_url.com"
/>
<Animatable(Grouping)
<Animatable(ResponsiveComponent)
animatableIndex={2}
items={
Array [

View File

@ -43,7 +43,7 @@ export class Day extends Component {
registerAnimatable: func,
deregisterAnimatable: func,
currentUser: shape(userShape),
}
};
constructor (props) {
super(props);

View File

@ -156,6 +156,17 @@ it('renders an activity notification when there is new activity', () => {
expect(nai.prop('title')).toBe(props.title);
});
it('does not render an activity notification when layout is not large', () => {
const props = getDefaultProps();
props.items[1].newActivity = true;
props.responsiveSize = 'medium';
const wrapper = shallow(
<Grouping {...props} />
);
const nai = wrapper.find('Animatable(NewActivityIndicator)');
expect(nai).toHaveLength(0);
});
it('renders a danger activity notification when there is a missing item', () => {
const props = getDefaultProps();
props.items[1].status = {missing: true};

View File

@ -1,10 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`grouping contains link pointing to course url 1`] = `
<div>
<div
className=""
/>
<div
className=""
>
<NotificationBadge />
<a
className="undefined undefined"
href="example.com"
@ -48,6 +48,7 @@ exports[`grouping contains link pointing to course url 1`] = `
courseName="Board Games"
date={"2017-04-25T13:06:07.000Z"}
id="5"
showNotificationBadge={false}
theme={
Object {
"iconColor": "#5678",
@ -73,6 +74,7 @@ exports[`grouping contains link pointing to course url 1`] = `
courseName="Board Games"
date={"2017-04-25T13:06:07.000Z"}
id="6"
showNotificationBadge={false}
theme={
Object {
"iconColor": "#5678",
@ -89,10 +91,10 @@ exports[`grouping contains link pointing to course url 1`] = `
`;
exports[`renders a CompletedItemsFacade when completed items are present by default 1`] = `
<div>
<div
className=""
/>
<div
className=""
>
<NotificationBadge />
<a
className="undefined undefined"
href="example.com"
@ -135,6 +137,7 @@ exports[`renders a CompletedItemsFacade when completed items are present by defa
courseName="Board Games"
date={"2017-04-25T13:06:07.000Z"}
id="6"
showNotificationBadge={false}
theme={
Object {
"iconColor": "#5678",
@ -156,6 +159,7 @@ exports[`renders a CompletedItemsFacade when completed items are present by defa
}
badges={Array []}
itemCount={1}
notificationBadge="none"
onClick={[Function]}
/>
</li>
@ -164,10 +168,10 @@ exports[`renders a CompletedItemsFacade when completed items are present by defa
`;
exports[`renders the base component with required props 1`] = `
<div>
<div
className=""
/>
<div
className=""
>
<NotificationBadge />
<a
className="undefined undefined"
href="example.com"
@ -211,6 +215,7 @@ exports[`renders the base component with required props 1`] = `
courseName="Board Games"
date={"2017-04-25T13:06:07.000Z"}
id="5"
showNotificationBadge={false}
theme={
Object {
"iconColor": "#5678",
@ -236,6 +241,7 @@ exports[`renders the base component with required props 1`] = `
courseName="Board Games"
date={"2017-04-25T13:06:07.000Z"}
id="6"
showNotificationBadge={false}
theme={
Object {
"iconColor": "#5678",
@ -252,10 +258,10 @@ exports[`renders the base component with required props 1`] = `
`;
exports[`renders to do items correctly 1`] = `
<div>
<div
className=""
/>
<div
className=""
>
<NotificationBadge />
<span>
<span
className=""
@ -284,6 +290,7 @@ exports[`renders to do items correctly 1`] = `
courseName={null}
date={"2017-06-16T11:06:07.000Z"}
id="700"
showNotificationBadge={false}
theme={
Object {
"iconColor": null,

View File

@ -18,20 +18,19 @@
import React, { Component } from 'react';
import themeable from '@instructure/ui-themeable/lib';
import classnames from 'classnames';
import containerQuery from '@instructure/ui-utils/lib/react/containerQuery';
import { partition } from 'lodash';
import { arrayOf, string, number, shape, bool, func } from 'prop-types';
import { userShape, itemShape } from '../plannerPropTypes';
import { arrayOf, string, number, shape, func } from 'prop-types';
import { userShape, itemShape, sizeShape } from '../plannerPropTypes';
import styles from './styles.css';
import theme from './theme.js';
import PlannerItem from '../PlannerItem';
import CompletedItemsFacade from '../CompletedItemsFacade';
import NewActivityIndicator from './NewActivityIndicator';
import MissingIndicator from './MissingIndicator';
import NotificationBadge, { MissingIndicator, NewActivityIndicator } from '../NotificationBadge';
import moment from 'moment-timezone';
import formatMessage from '../../format-message';
import { getBadgesForItem, getBadgesForItems, showPillForOverdueStatus } from '../../utilities/statusUtils';
import { animatable } from '../../dynamic-ui';
import responsiviser from '../responsiviser';
export class Grouping extends Component {
static propTypes = {
@ -47,19 +46,22 @@ export class Grouping extends Component {
registerAnimatable: func,
deregisterAnimatable: func,
currentUser: shape(userShape),
}
responsiveSize: sizeShape,
};
static defaultProps = {
registerAnimatable: () => {},
deregisterAnimatable: () => {},
}
responsiveSize: 'large',
};
constructor (props) {
super(props);
this.state = {
showCompletedItems: false,
badgeMap: this.setupItemBadgeMap(props.items)
badgeMap: this.setupItemBadgeMap(props.items),
};
}
componentDidMount () {
@ -102,13 +104,25 @@ export class Grouping extends Component {
});
}
getLayout() {
return this.props.responsiveSize;
}
renderItemsAndFacade (items) {
const [completedItems, otherItems ] = partition(items, item => (item.completed && !item.show));
let itemsToRender = otherItems;
if (this.state.showCompletedItems) {
itemsToRender = items;
}
const componentsToRender = itemsToRender.map((item, itemIndex) => (
const componentsToRender = this.renderItems(itemsToRender);
componentsToRender.push(this.renderFacade(completedItems, itemsToRender.length));
return componentsToRender;
}
renderItems (items) {
const showNotificationBadgeOnItem = this.getLayout() !== 'large';
return items.map((item, itemIndex) => (
<li
className={styles.item}
key={item.uniqueId}
@ -135,32 +149,51 @@ export class Grouping extends Component {
badges={this.state.badgeMap[item.id]}
details={item.details}
toggleAPIPending={item.toggleAPIPending}
status={item.status}
newActivity={item.newActivity}
showNotificationBadge={showNotificationBadgeOnItem}
currentUser={this.props.currentUser}
/>
</li>
));
}
renderFacade (completedItems, animatableIndex) {
const showNotificationBadgeOnItem = this.getLayout() !== 'large';
if (!this.state.showCompletedItems && completedItems.length > 0) {
// Super odd that this is keyed on length? Sure it is. But there should
// only ever be one in our grouping and this keeps react from complaining
const completedItemIds = completedItems.map(item => item.uniqueId);
componentsToRender.push(
let missing = false;
let newActivity = false;
const completedItemIds = completedItems.map(item => {
if (showPillForOverdueStatus('missing', item)) missing = true;
if (item.newActivity) newActivity = true;
return item.uniqueId;
});
let notificationBadge = 'none';
if (showNotificationBadgeOnItem) {
if (newActivity) {
notificationBadge = 'newActivity';
} else if (missing) {
notificationBadge = 'missing';
}
}
return (
<li
className={styles.item}
key={`length-${completedItems.length}`}
key='completed'
>
<CompletedItemsFacade
onClick={this.handleFacadeClick}
itemCount={completedItems.length}
badges={getBadgesForItems(completedItems)}
animatableIndex={itemsToRender.length}
animatableIndex={animatableIndex}
animatableItemIds={completedItemIds}
notificationBadge={notificationBadge}
/>
</li>
);
}
return componentsToRender;
return null;
}
renderToDoText () {
@ -168,6 +201,11 @@ export class Grouping extends Component {
}
renderNotificationBadge () {
// narrower layout puts the indicator next to the actual items
if (this.getLayout() !== 'large') {
return null;
}
let missing = false;
const newItem = this.props.items.find(item => {
if (showPillForOverdueStatus('missing', item)) missing = true;
@ -187,12 +225,12 @@ export class Grouping extends Component {
// I wouldn't have broken the background and title apart, but wrapping them in a container span breaks styling
renderGroupLinkBackground() {
return <span className={classnames({
const clazz = classnames({
[styles.overlay]: true,
[styles.withImage]: this.props.image_url
})}
style={{ backgroundColor: this.props.color }}
/>;
});
const style = this.getLayout() === 'large' ? { backgroundColor: this.props.color } : null;
return <span className={clazz} style={style} />;
}
renderGroupLinkTitle() {
@ -208,11 +246,12 @@ export class Grouping extends Component {
{this.renderGroupLinkTitle()}
</span>;
}
const style = this.getLayout() === 'large' ? {backgroundImage: `url(${this.props.image_url || ''})`} : null;
return <a
href={this.props.url || "#"}
ref={this.groupingLinkRef}
className={`${styles.hero} ${styles.heroHover}`}
style={{backgroundImage: `url(${this.props.image_url || ''})`}}
style={style}
>
{this.renderGroupLinkBackground()}
{this.renderGroupLinkTitle()}
@ -222,18 +261,9 @@ export class Grouping extends Component {
render () {
const badge = this.renderNotificationBadge();
const activityIndicatorClasses = {
[styles.activityIndicator]: true,
[styles.hasBadge]: badge != null
};
return (
<div className={styles.root}>
<div
className={classnames(activityIndicatorClasses)}
>
{badge}
</div>
<div className={classnames(styles.root, styles[this.getLayout()])}>
<NotificationBadge>{badge}</NotificationBadge>
{this.renderGroupLink()}
<ol className={styles.items} style={{ borderColor: this.props.color }}>
{ this.renderItemsAndFacade(this.props.items)}
@ -243,11 +273,6 @@ export class Grouping extends Component {
}
}
export default animatable(themeable(theme, styles)(
// we can update this to be whatever works for this component and its content
containerQuery({
'media-x-large': { minWidth: '68rem' },
'media-large': { minWidth: '58rem' },
'media-medium': { minWidth: '48rem' }
})(Grouping)
));
const ResponsiveGrouping = responsiviser()(Grouping);
export default animatable(themeable(theme, styles)(ResponsiveGrouping));

View File

@ -7,6 +7,7 @@
color: var(--groupColor);
line-height: var(--lineHeight);
position: relative;
display: flex;
}
.title {
@ -44,10 +45,11 @@
.hero {
position: relative;
display: flex;
flex: 0 0 var(--heroWidth);
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
@ -55,15 +57,20 @@
padding: var(--heroPadding);
text-decoration: none;
/* this can become min-height once we drop IE11 support */
height: var(--heroMinHeight);
/* handle long words that break layout */
min-width: 1px;
.groupingName {
text-decoration: var(--heroLinkTextDecoration);
}
}
.hero,
.overlay {
border-bottom-left-radius: var(--heroBorderRadius);
border-top-left-radius: var(--heroBorderRadius);
}
.heroHover {
&:focus,
&:hover {
@ -89,71 +96,36 @@
}
}
/* the <ol> */
.items {
flex: 1;
list-style-type: none;
margin: 0;
padding: 0;
border-top: var(--borderTopWidth) solid;
border-color: var(--groupColor);
color: var(--groupColor);
}
.activityIndicator.hasBadge {
background: var(--activityIndicatorBackground);
width: var(--activityIndicatorBorderSize);
height: var(--activityIndicatorBorderSize);
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: -0.25rem;
right: -0.25rem;
z-index: 1;
border-radius: 100%;
}
[data-media-medium] {
.medium {
&.root {
display: flex;
display: block;
}
.hero, .overlay {
border-radius: 0;
background-color: transparent;
}
.hero {
flex: 0 0 var(--heroWidth);
height: auto;
/* handle long words that break layout */
min-width: 1px;
display: block;
flex: none;
min-height: unset;
line-height: 2rem;
}
.hero,
.overlay {
border-bottom-left-radius: var(--heroBorderRadius);
border-top-left-radius: var(--heroBorderRadius);
.title {
font-size: var(--titleFontSizeTablet);
padding-left: 0;
}
.items {
flex: 1;
border-top: var(--borderTopWidthTabletUp) solid;
}
.activityIndicator {
width: var(--activityIndicatorWidth);
padding: var(--activityIndicatorPadding);
&.hasBadge {
background: transparent;
width: auto;
height: auto;
position: static;
top: auto;
right: auto;
z-index: auto;
border-radius: 0;
}
}
}
[data-media-x-large] {
.hero {
flex: 0 0 var(--heroWidthLarge);
border-top-width: var(--borderTopWidthTablet);
}
}

View File

@ -25,7 +25,8 @@ export default function generator ({ borders, colors, media, spacing, typography
groupColor: colors.brand,
borderTopWidthTabletUp: borders.widthSmall,
borderTopWidth: borders.widthSmall,
borderTopWidthTablet: borders.widthMedium,
heroMinHeight: '7rem',
heroWidth: '12rem',
@ -37,6 +38,7 @@ export default function generator ({ borders, colors, media, spacing, typography
overlayOpacity: 0.75,
titleFontSize: typography.fontSizeXSmall,
titleFontSizeTablet: '0.875rem',
titleFontWeight: typography.fontWeightBold,
titleLetterSpacing: '0.0625rem',
titleBackground: colors.white,
@ -46,10 +48,6 @@ export default function generator ({ borders, colors, media, spacing, typography
titleTextDecoration: 'none',
titleTextDecorationHover: 'underline',
titleColor: colors.brand,
activityIndicatorPadding: spacing.small,
activityIndicatorWidth: spacing.small,
activityIndicatorBorderSize: '1rem',
activityIndicatorBackground: colors.white,
...media
};
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2018 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import { shallow } from 'enzyme';
import NotificationBadge, {NewActivityIndicator} from '../index';
// it would be better if the snapshots contained the proper class names, but
// jest doesn't deal with how themeable turns styles.css into code.
it('renders an indicator', () => {
const wrapper = shallow(<NotificationBadge><NewActivityIndicator title="blah"/></NotificationBadge>);
expect(wrapper).toMatchSnapshot();
});
it('renders an empty div', () => {
const wrapper = shallow(<NotificationBadge>{null}</NotificationBadge>);
expect(wrapper).toMatchSnapshot();
});

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders an empty div 1`] = `
<div
className=""
/>
`;
exports[`renders an indicator 1`] = `
<div
className="undefined"
>
<Animatable(NewActivityIndicator)
title="blah"
/>
</div>
`;

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import themeable from '@instructure/ui-themeable/lib';
import MissingIndicator from './MissingIndicator';
import NewActivityIndicator from './NewActivityIndicator';
import styles from './styles.css';
import theme from './theme.js';
class NotificationBadge extends React.Component {
static propTypes = {
children: PropTypes.element
};
render () {
const indicator = this.props.children ? React.Children.only(this.props.children) : null;
const activityIndicatorClasses = {
[styles.activityIndicator]: true,
[styles.hasBadge]: indicator != null
};
return (
<div className={classnames(activityIndicatorClasses)}>
{indicator}
</div>
);
}
}
const ThemeableNotificationBadge = themeable(theme, styles)(NotificationBadge);
export {
MissingIndicator,
NewActivityIndicator,
NotificationBadge,
};
export default ThemeableNotificationBadge;

View File

@ -0,0 +1,21 @@
.root {
}
.activityIndicator {
width: var(--activityIndicatorWidth);
padding: var(--activityIndicatorPadding);
&.hasBadge {
background: transparent;
width: auto;
height: auto;
align-items: center;
justify-content: center;
position: static;
display: flex;
top: auto;
right: auto;
z-index: 1;
border-radius: 0;
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2018 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export default function generator ({ colors, spacing }) {
return {
activityIndicatorPadding: spacing.small,
activityIndicatorWidth: spacing.small,
activityIndicatorBorderSize: '1rem',
activityIndicatorBackground: colors.white,
};
}

View File

@ -2,7 +2,7 @@
exports[`renders base component using dayKeys 1`] = `
<div
className="PlannerApp"
className="PlannerApp large"
>
<ShowOnFocusButton
buttonProps={
@ -67,7 +67,9 @@ exports[`renders base component using dayKeys 1`] = `
`;
exports[`renders empty component with no assignments 1`] = `
<div>
<div
className="PlannerApp large"
>
<div>
<ShowOnFocusButton
buttonProps={
@ -88,7 +90,7 @@ exports[`renders empty component with no assignments 1`] = `
exports[`shows new activity button when new activity is indicated 1`] = `
<div
className="PlannerApp"
className="PlannerApp large"
>
<StickyButton
buttonRef={[Function]}
@ -162,7 +164,7 @@ exports[`shows new activity button when new activity is indicated 1`] = `
exports[`shows only the loading component when the isLoading prop is true 1`] = `
<div
className="PlannerApp"
className="PlannerApp large"
>
<ShowOnFocusButton
buttonProps={
@ -197,7 +199,7 @@ exports[`shows only the loading component when the isLoading prop is true 1`] =
exports[`shows the loading past indicator when loadingPast prop is true 1`] = `
<div
className="PlannerApp"
className="PlannerApp large"
>
<ShowOnFocusButton
buttonProps={

View File

@ -16,12 +16,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { Component } from 'react';
import classnames from 'classnames';
import { connect } from 'react-redux';
import Container from '@instructure/ui-core/lib/components/Container';
import Spinner from '@instructure/ui-core/lib/components/Spinner';
import { arrayOf, oneOfType, shape, bool, object, string, number, func } from 'prop-types';
import { momentObj } from 'react-moment-proptypes';
import { userShape } from '../plannerPropTypes';
import { userShape, sizeShape } from '../plannerPropTypes';
import Day from '../Day';
import ShowOnFocusButton from '../ShowOnFocusButton';
import StickyButton from '../StickyButton';
@ -64,14 +65,15 @@ export class PlannerApp extends Component {
naiAboveScreen: bool,
}),
currentUser: shape(userShape),
size: sizeShape,
};
static defaultProps = {
isLoading: false,
stickyOffset: 0,
triggerDynamicUiUpdates: () => {},
preTriggerDynamicUiUpdates: () => {},
plannerActive: () => {return false;}
plannerActive: () => {return false;},
size: 'large',
};
componentWillUpdate () {
@ -165,16 +167,16 @@ export class PlannerApp extends Component {
);
}
renderBody (children) {
renderBody (children, classes) {
if (children.length === 0) {
return <div>
return <div className={classes}>
{this.renderNewActivity()}
{this.renderNoAssignments()}
</div>;
}
return <div className="PlannerApp">
return <div className={classes}>
{this.renderNewActivity()}
{this.renderLoadPastButton()}
{this.renderLoadingPast()}
@ -185,24 +187,25 @@ export class PlannerApp extends Component {
}
render () {
const clazz = classnames('PlannerApp', this.props.size);
let children;
if (this.props.isLoading) {
return this.renderBody(this.renderLoading());
children = this.renderLoading();
} else {
children = this.props.days.map(([dayKey, dayItems], dayIndex) => {
return <Day
timeZone={this.props.timeZone}
day={dayKey}
itemsForDay={dayItems}
animatableIndex={dayIndex}
key={dayKey}
toggleCompletion={this.props.togglePlannerItemCompletion}
updateTodo={this.props.updateTodo}
currentUser={this.props.currentUser}
/>;
});
}
const children = this.props.days.map(([dayKey, dayItems], dayIndex) => {
return <Day
timeZone={this.props.timeZone}
day={dayKey}
itemsForDay={dayItems}
animatableIndex={dayIndex}
key={dayKey}
toggleCompletion={this.props.togglePlannerItemCompletion}
updateTodo={this.props.updateTodo}
currentUser={this.props.currentUser}
/>;
});
return this.renderBody(children);
return this.renderBody(children, clazz);
}
}

View File

@ -551,3 +551,13 @@ it('registers itself as animatable', () => {
wrapper.unmount();
expect(fakeDeregister).toHaveBeenCalledWith('item', instance, ['second']);
});
it('renders a NewActivityIndicator when asked to', () => {
const props = defaultProps({points: 35, date: DEFAULT_DATE});
props.newActivity = true;
props.showNotificationBadge = true;
const wrapper = shallow(
<PlannerItem {...props} />
);
expect(wrapper.find('Animatable(NewActivityIndicator)')).toHaveLength(1);
});

View File

@ -2,6 +2,7 @@
exports[`renders Announcement correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -98,6 +99,7 @@ exports[`renders Announcement correctly with everything 1`] = `
exports[`renders Announcement correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -170,6 +172,7 @@ exports[`renders Announcement correctly with just date 1`] = `
exports[`renders Announcement correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -264,6 +267,7 @@ exports[`renders Announcement correctly with just points 1`] = `
exports[`renders Announcement correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -334,6 +338,7 @@ exports[`renders Announcement correctly without right side content 1`] = `
exports[`renders Assignment correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -430,6 +435,7 @@ exports[`renders Assignment correctly with everything 1`] = `
exports[`renders Assignment correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -502,6 +508,7 @@ exports[`renders Assignment correctly with just date 1`] = `
exports[`renders Assignment correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -596,6 +603,7 @@ exports[`renders Assignment correctly with just points 1`] = `
exports[`renders Assignment correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -666,6 +674,7 @@ exports[`renders Assignment correctly without right side content 1`] = `
exports[`renders Calendar Event correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -762,6 +771,7 @@ exports[`renders Calendar Event correctly with everything 1`] = `
exports[`renders Calendar Event correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -834,6 +844,7 @@ exports[`renders Calendar Event correctly with just date 1`] = `
exports[`renders Calendar Event correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -928,6 +939,7 @@ exports[`renders Calendar Event correctly with just points 1`] = `
exports[`renders Calendar Event correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -998,6 +1010,7 @@ exports[`renders Calendar Event correctly without right side content 1`] = `
exports[`renders Discussion correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -1094,6 +1107,7 @@ exports[`renders Discussion correctly with everything 1`] = `
exports[`renders Discussion correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1166,6 +1180,7 @@ exports[`renders Discussion correctly with just date 1`] = `
exports[`renders Discussion correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1260,6 +1275,7 @@ exports[`renders Discussion correctly with just points 1`] = `
exports[`renders Discussion correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1330,6 +1346,7 @@ exports[`renders Discussion correctly without right side content 1`] = `
exports[`renders Note correctly with Group 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1402,6 +1419,7 @@ exports[`renders Note correctly with Group 1`] = `
exports[`renders Note correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -1480,6 +1498,7 @@ exports[`renders Note correctly with everything 1`] = `
exports[`renders Note correctly without Course 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1556,6 +1575,7 @@ exports[`renders Note correctly without Course 1`] = `
exports[`renders Page correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -1652,6 +1672,7 @@ exports[`renders Page correctly with everything 1`] = `
exports[`renders Page correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1724,6 +1745,7 @@ exports[`renders Page correctly with just date 1`] = `
exports[`renders Page correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1818,6 +1840,7 @@ exports[`renders Page correctly with just points 1`] = `
exports[`renders Page correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -1888,6 +1911,7 @@ exports[`renders Page correctly without right side content 1`] = `
exports[`renders Quiz correctly with everything 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={true}
@ -1984,6 +2008,7 @@ exports[`renders Quiz correctly with everything 1`] = `
exports[`renders Quiz correctly with just date 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -2056,6 +2081,7 @@ exports[`renders Quiz correctly with just date 1`] = `
exports[`renders Quiz correctly with just points 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -2150,6 +2176,7 @@ exports[`renders Quiz correctly with just points 1`] = `
exports[`renders Quiz correctly without right side content 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}
@ -2220,6 +2247,7 @@ exports[`renders Quiz correctly without right side content 1`] = `
exports[`renders correctly 1`] = `
<div>
<NotificationBadge />
<div>
<Checkbox
checked={false}

View File

@ -17,7 +17,6 @@
*/
import React, { Component } from 'react';
import themeable from '@instructure/ui-themeable/lib';
import containerQuery from '@instructure/ui-utils/lib/react/containerQuery';
import Text from '@instructure/ui-core/lib/components/Text';
import Checkbox from '@instructure/ui-core/lib/components/Checkbox';
import Link from '@instructure/ui-core/lib/components/Link';
@ -30,11 +29,13 @@ import Announcement from 'instructure-icons/lib/Line/IconAnnouncementLine';
import Discussion from 'instructure-icons/lib/Line/IconDiscussionLine';
import Calendar from 'instructure-icons/lib/Line/IconCalendarMonthLine';
import Page from 'instructure-icons/lib/Line/IconMsWordLine';
import NotificationBadge, { MissingIndicator, NewActivityIndicator } from '../NotificationBadge';
import BadgeList from '../BadgeList';
import styles from './styles.css';
import theme from './theme.js';
import { arrayOf, bool, number, string, func, shape, object } from 'prop-types';
import { badgeShape, userShape } from '../plannerPropTypes';
import { badgeShape, userShape, statusShape } from '../plannerPropTypes';
import { showPillForOverdueStatus } from '../../utilities/statusUtils';
import { momentObj } from 'react-moment-proptypes';
import formatMessage from '../../format-message';
import {animatable} from '../../dynamic-ui';
@ -61,6 +62,9 @@ export class PlannerItem extends Component {
registerAnimatable: func,
deregisterAnimatable: func,
toggleAPIPending: bool,
status: statusShape,
newActivity: bool,
showNotificationBadge: bool,
currentUser: shape(userShape),
};
@ -220,6 +224,31 @@ export class PlannerItem extends Component {
);
}
renderNotificationBadge () {
if (!this.props.showNotificationBadge) {
return null;
}
const newItem = this.props.newActivity;
let missing = false;
if (showPillForOverdueStatus('missing', {status: this.props.status, context: this.props.context})) {
missing = true;
}
if (newItem || missing) {
const IndicatorComponent = newItem ? NewActivityIndicator : MissingIndicator;
return (
<div className={styles.activityIndicator}>
<IndicatorComponent
title={this.props.title}
itemIds={[this.props.uniqueId]}
animatableIndex={this.props.animatableIndex} />
</div>
);
} else {
return null;
}
}
render () {
const assignmentType = this.props.associated_item ?
@ -231,6 +260,7 @@ export class PlannerItem extends Component {
{ assignmentType: assignmentType, title: this.props.title });
return (
<div className={styles.root} ref={this.registerRootDivRef}>
<NotificationBadge>{this.renderNotificationBadge()}</NotificationBadge>
<div className={styles.completed}>
<Checkbox
ref={this.registerFocusElementRef}
@ -256,11 +286,4 @@ export class PlannerItem extends Component {
}
}
export default animatable(themeable(theme, styles)(
// we can update this to be whatever works for this component and its content
containerQuery({
'media-x-large': { minWidth: '68rem' },
'media-large': { minWidth: '58rem' },
'media-medium': { minWidth: '48rem' }
})(PlannerItem))
);
export default animatable(themeable(theme, styles)(PlannerItem));

View File

@ -22,6 +22,15 @@
margin-left: var(--gutterWidth);
}
.activityIndicator {
padding-right: 0;
padding-left: 0;
}
.activityIndicator + .completed {
margin-left: calc(var(--gutterWidth) - var(--activityIndicatorWidth))
}
.icon {
color: var(--iconColor);
font-size: var(--iconFontSize);
@ -38,17 +47,22 @@
}
.layout {
display: flex;
flex: 1;
align-items: center;
min-width: 1px;
}
.details {
flex: 0 0 50%;
margin-bottom: 0;
box-sizing: border-box;
margin-bottom: var(--bottomMargin);
min-width: 1px;
}
.secondary {
flex: 0 0 50%;
box-sizing: border-box;
display: flex;
align-items: center;
@ -79,7 +93,7 @@
.metrics {
box-sizing: border-box;
text-align: right;
flex: 1;
flex: 0 0 7rem;
min-width: 1px;
padding-left: var(--metricsPadding);
}
@ -95,44 +109,6 @@
.badges {
flex: 1;
text-align: end;
min-width: 1px;
}
[data-media-medium] {
&.root {
padding: var(--paddingMedium);
}
.layout {
display: flex;
align-items: center;
}
.details {
flex: 0 0 50%;
margin-bottom: 0;
}
.secondary {
flex: 0 0 50%;
}
.badges {
text-align: end;
}
.metrics {
flex: 0 0 7rem;
}
}
[data-media-x-large] {
&.root {
padding: var(--paddingLarge);
}
.completed,
.icon {
margin-right: var(--gutterWidthLarge);
}
}

View File

@ -41,7 +41,7 @@ export default function generator ({ borders, colors, spacing, typography }) {
typeMargin: spacing.xxxSmall,
titleLineHeight: typography.lineHeightFit
titleLineHeight: typography.lineHeightFit,
};
}

View File

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders large 1`] = `
<ResponsiveComponent
responsiveSize="large"
>
<SomeComponent
responsiveSize="large"
>
<div
data-sz="large"
>
hello world
</div>
</SomeComponent>
</ResponsiveComponent>
`;
exports[`renders medium 1`] = `
<ResponsiveComponent
responsiveSize="large"
>
<SomeComponent
responsiveSize="large"
>
<div
data-sz="large"
>
hello world
</div>
</SomeComponent>
</ResponsiveComponent>
`;
exports[`renders medium 2`] = `
<ResponsiveComponent
responsiveSize="large"
>
<SomeComponent
responsiveSize="medium"
>
<div
data-sz="medium"
>
hello world
</div>
</SomeComponent>
</ResponsiveComponent>
`;

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2108 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import {string} from 'prop-types';
import { mount } from 'enzyme';
import responsiviser from '../responsiviser';
jest.useFakeTimers();
const defaultWindowWidth = window.innerWidth;
let mockMatchMedia = false;
let handleWindowResize = null;
function resizeWindow(newWidth) {
window.innerWidth = newWidth;
window.dispatchEvent(new Event('resize'));
jest.runAllTimers();
}
function mockMediaQueryList(mediaQuery) {
this.mediaQuery = mediaQuery;
this.matches = window.innerWidth <= 768;
this.onWindowResize = (event) => {
this.matches = window.innerWidth <= 768;
};
handleWindowResize = this.onWindowResize;
window.addEventListener('resize', this.onWindowResize);
}
function mockUpWindow () {
if ('matchMedia' in window) return;
mockMatchMedia = true;
window.matchMedia = function(mediaQuery) {
return new mockMediaQueryList(mediaQuery);
};
window.innerWidth = 1024;
}
function resetWindow () {
window.innerWidth = defaultWindowWidth;
if(mockMatchMedia) {
window.removeEventListener('resize', handleWindowResize);
delete window.matchMedia;
}
}
class SomeComponent extends React.Component {
static propTypes = { responsiveSize: string }
static defaultProps = { responsiveSize: 'large' }
render () {
return <div data-sz={this.props.responsiveSize}>hello world</div>;
}
}
beforeAll(() => {
mockUpWindow();
});
afterAll(() => {
resetWindow();
});
it('renders large', () => {
const ResponsiveComponent = responsiviser()(SomeComponent);
const wrapper = mount(<ResponsiveComponent/>);
expect(responsiviser.mqwatcher.interestedParties).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
wrapper.unmount();
expect(responsiviser.mqwatcher.interestedParties).toHaveLength(0);
});
it('renders medium', () => {
debugger;
const ResponsiveComponent = responsiviser()(SomeComponent);
const wrapper = mount(<ResponsiveComponent/>);
expect(wrapper).toMatchSnapshot(); // large
resizeWindow(700);
expect(wrapper).toMatchSnapshot(); // medium
wrapper.unmount();
});

View File

@ -46,10 +46,27 @@ export const opportunityShape = {
nextUrl: PropTypes.string,
};
export const sizeShape = PropTypes.oneOf(['medium', 'large']);
export const statusShape = PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
excused: PropTypes.bool,
graded: PropTypes.bool,
has_feedback: PropTypes.bool,
late: PropTypes.bool,
missing: PropTypes.bool,
needs_grading: PropTypes.bool,
submitted: PropTypes.bool,
})
]);
export default {
badgeShape,
userShape,
courseShape,
itemShape,
opportunityShape
opportunityShape,
sizeShape,
statusShape,
};

View File

@ -0,0 +1,141 @@
/*
* Copyright (C) 2018 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
// Watches for changes in the match state of a media-query
class MediaQueryWatcher {
size = 'large';
interestedParties = [];
// initialize the mediaQueryList with our media-query of interest
setup () {
if (!window.matchMedia) return; // or unit tests fail
this.mediaQueryList = window.matchMedia('(max-width: 50em)'); // hard-code for now
this.size = this.mediaQueryList.matches ? 'medium' : 'large';
// some browsers support mediaQueryList.onchange. Use it if we can
if ('onchange' in this.mediaQueryList) {
this.mediaQueryList.onchange = (event) => {
this.onChangeSize(event);
};
} else {
// add a window.resize event handler. When the user stops
// resizing for 100ms, check the state of the mediaQueryList's
// match state.
this.handleResize = () => {
window.clearTimeout(this.resizeTimer);
this.resizeTimer = window.setTimeout(() => {
this.resizeTimer = 0;
this.onChangeSize(this.mediaQueryList);
}, 100);
};
this.elementResizeListener = window.addEventListener('resize', this.handleResize);
}
}
teardown () {
if ('onchange' in this.mediaQueryList) {
this.mediaQueryList.onchange = null;
} else {
window.clearTimeout(this.resizeTimer);
window.removeEventListener('resize', this.handleResize);
}
}
// add a component that's interested in being notified when the media-query
// match state changes
add (interestedParty) {
if (!this.mediaQueryList) {
this.setup();
}
this.interestedParties.push(interestedParty);
return this.size;
}
// remove a component that's no longer interested
remove (interestedParty) {
const i = this.interestedParties.indexOf(interestedParty);
this.interestedParties.splice(i, 1);
if (this.mediaQueryList && this.interestedParties.length === 0) {
this.teardown();
this.mediaQueryList = null;
}
}
// tell everyone that's interested something has changed
notifyAll () {
this.interestedParties.forEach((g) => {
g.onChangeSize({size: this.size});
});
}
// we just noticed a change in media-query match state
onChangeSize (event) {
const newSize = event.matches ? 'medium' : 'large';
if (newSize !== this.size) {
this.size = newSize;
this.notifyAll();
}
}
}
// take any react component have it respond to media query state
// e.g. const ResponsiveFoo = responsiviser()(Foo)
// The media query is currently hard-coded to deal with medium v. large
// rendering of Grouping, but could be extended to have a map of
// MediaQueryWatchers for each one. We'll add that complication if it
// ever becomes necessary.
// This has the advantage over instui Responsive in that it only requires
// one listener and has interested parties register to be notified of
// a change in state.
function responsiviser () {
return function (ComposedComponent) {
class ResponsiveComponent extends React.Component {
static propTypes = {
...ComposedComponent.propTypes
}
static defaultProps = ComposedComponent.defaultProps ? {...ComposedComponent.defaultProps} : null;
static displayName = `Responsive${ComposedComponent.displayName}`;
static name() {
return `Responsive${ComposedComponent.displayName}`;
}
constructor (props) {
super(props);
const size = responsiviser.mqwatcher.add(this);
this.state = {
size,
};
}
componentWillUnmount () {
responsiviser.mqwatcher.remove(this);
}
onChangeSize (event) {
this.setState({size: event.size});
}
render () {
return <ComposedComponent {...this.props} responsiveSize={this.state.size} />;
}
}
return ResponsiveComponent;
};
}
responsiviser.mqwatcher = new MediaQueryWatcher(); // this one and only one for now
export default responsiviser;

View File

@ -1,5 +1,6 @@
module.exports = {
generateScopedName: function ({ env }) { // for css modules class names
return (env === 'production') ? '[hash:base64]' : '[folder]-[name]__[local]';
const env2 = process.env.NODE_ENV || env; // because what sets the env arg prefers BABEL_ENV over NODE_ENV
return (env2 === 'production') ? '[hash:base64]' : '[folder]-[name]__[local]';
}
};

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ module PlannerPageObject
end
def select_list_view
fxpath("//span[text()[contains(.,'List View')]]").click
fxpath("//span[contains(text(),'List View')]").click
end
def select_dashboard_view
@ -48,7 +48,7 @@ module PlannerPageObject
# Pass what type of object it is. Ensure object's name starts with a capital letter
def validate_object_displayed(object_type)
expect(fxpath("//*[@class='PlannerApp']//span[contains(text(),'Unnamed Course #{object_type}')]")).to be_displayed
expect(fxpath("//*[contains(@class, 'PlannerApp')]//span[contains(text(),'Unnamed Course #{object_type}')]")).to be_displayed
end
def validate_no_due_dates_assigned
@ -65,11 +65,11 @@ module PlannerPageObject
end
def expand_completed_item
fxpath('//*[@class="PlannerApp"]//*[contains(text(),"Show 1 completed item")]').click
fxpath('//*[contains(@class, "PlannerApp")]//*[contains(text(),"Show 1 completed item")]').click
end
def validate_pill(pill_type)
expect(fxpath("//*[@class='PlannerApp']//*[contains(text(),'#{pill_type}')]")).to be_displayed
expect(fxpath("//*[contains(@class, 'PlannerApp')]//*[contains(text(),'#{pill_type}')]")).to be_displayed
end
def go_to_list_view

View File

@ -118,7 +118,7 @@ describe "student planner" do
it "ensures time zone changes update the planner items", priority: "1", test_id: 3306207 do
go_to_list_view
time = calendar_time_string(@assignment.due_at).chop
expect(fxpath("//div[@class='PlannerApp']//span[text()[contains(.,'DUE: #{time}')]]")).
expect(fxpath("//div[contains(@class, 'PlannerApp')]//span[contains(text(),'DUE: #{time}')]")).
to be_displayed
@student1.time_zone = 'Asia/Tokyo'
@student1.save!
@ -126,7 +126,7 @@ describe "student planner" do
# the users time zone is not converted to UTC and to balance it we subtract 6 hours from the due time
time = calendar_time_string(@assignment.due_at+9.hours).chop
expect(fxpath("//div[@class='PlannerApp']//span[text()[contains(.,'DUE: #{time}')]]")).
expect(fxpath("//div[contains(@class, 'PlannerApp')]//span[contains(text(),'DUE: #{time}')]")).
to be_displayed
end
@ -137,7 +137,7 @@ describe "student planner" do
force_click("button:contains('Load prior')")
planner = f('.PlannerApp')
expect(planner).to be_displayed
assn_element = fxpath("//span[text()[contains(.,'Unnamed Course Assignment')]]", planner)
assn_element = fxpath("//span[contains(text(),'Unnamed Course Assignment')]", planner)
expect(assn_element).to be_displayed
validate_pill('Missing')
end

View File

@ -88,7 +88,23 @@
normalize-path "^2.0.1"
through2 "^2.0.3"
"@instructure/ui-core@^4.1.0", "@instructure/ui-core@^4.7.3", "@instructure/ui-core@^4.8.0":
"@instructure/ui-core@^4.1.0", "@instructure/ui-core@^4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@instructure/ui-core/-/ui-core-4.7.3.tgz#849b85ccce71a22556cc43754d31ab4344de31a5"
dependencies:
"@instructure/ui-themeable" "^4.7.3"
"@instructure/ui-utils" "^4.7.3"
bowser "^1.7.0"
classnames "^2.2.5"
decimal.js "^7.2.1"
deep-equal "^1.0.1"
instructure-icons "^4.3.1"
keycode "^2.1.8"
no-scroll "^2.1.0"
numeral "^2.0.6"
prop-types "^15.5.10"
"@instructure/ui-core@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-core/-/ui-core-4.8.0.tgz#fb32698507f52fd77bb471c199018c1e8671d57b"
dependencies:
@ -104,6 +120,17 @@
numeral "^2.0.6"
prop-types "^15.5.10"
"@instructure/ui-themeable@^4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@instructure/ui-themeable/-/ui-themeable-4.7.3.tgz#d1732d54b0f64213cf521bb2875a4a28a015c69e"
dependencies:
"@instructure/ui-utils" "^4.7.3"
bowser "^1.7.0"
deep-equal "^1.0.1"
glamor "^2.20.37"
prop-types "^15.5.10"
tinycolor2 "^1.4.1"
"@instructure/ui-themeable@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-themeable/-/ui-themeable-4.8.0.tgz#8840fe7e923070924cda519167ee04009eb9901e"
@ -115,10 +142,31 @@
prop-types "^15.5.10"
tinycolor2 "^1.4.1"
"@instructure/ui-themes@^4.1.0", "@instructure/ui-themes@^4.7.3", "@instructure/ui-themes@^4.8.0":
"@instructure/ui-themes@^4.1.0", "@instructure/ui-themes@^4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@instructure/ui-themes/-/ui-themes-4.7.3.tgz#dca8c846bcaa47909755431577db1792ad4f9db3"
"@instructure/ui-themes@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-themes/-/ui-themes-4.8.0.tgz#37bf837e6497a2e75c75a1029318f993d0672579"
"@instructure/ui-utils@^4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@instructure/ui-utils/-/ui-utils-4.7.3.tgz#0484572d6d4203dde9158b495f276a73fd4a187b"
dependencies:
bowser "^1.7.0"
decimal.js "^7.2.1"
deep-equal "^1.0.1"
keycode "^2.1.8"
moment "^2.10.6"
moment-timezone "^0.5.14"
no-scroll "^2.1.0"
numeral "^2.0.6"
object.omit "^3.0.0"
object.pick "^1.2.0"
prop-types "^15.5.10"
shortid "^2.2.8"
"@instructure/ui-utils@^4.8.0":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-utils/-/ui-utils-4.8.0.tgz#c62082849aa64aff40d9ce897108b27767f8712f"