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:
parent
10c390a2ac
commit
f7be7df9a6
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}]]
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -43,7 +43,7 @@ export class Day extends Component {
|
|||
registerAnimatable: func,
|
||||
deregisterAnimatable: func,
|
||||
currentUser: shape(userShape),
|
||||
}
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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={
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function generator ({ borders, colors, spacing, typography }) {
|
|||
|
||||
typeMargin: spacing.xxxSmall,
|
||||
|
||||
titleLineHeight: typography.lineHeightFit
|
||||
titleLineHeight: typography.lineHeightFit,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
52
yarn.lock
52
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue