Skip to content

Commit

Permalink
Merge pull request #8040 from jonboiser/custom-channels-theming
Browse files Browse the repository at this point in the history
Add ContentModal to display resources within the context of a Custom Channel Renderer
  • Loading branch information
rtibbles committed May 4, 2021
2 parents dfef3fb + 1e8a18e commit 79ca8e4
Show file tree
Hide file tree
Showing 17 changed files with 675 additions and 58 deletions.
2 changes: 2 additions & 0 deletions kolibri/core/assets/src/core-app/apiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import bytesForHumans from '../utils/bytesForHumans';
import UserType from '../utils/UserType';
import samePageCheckGenerator from '../utils/samePageCheckGenerator';
import AppBar from '../views/AppBar';
import Backdrop from '../views/Backdrop';
import CoreSnackbar from '../views/CoreSnackbar';
import CoreMenu from '../views/CoreMenu';
import CoreMenuDivider from '../views/CoreMenu/CoreMenuDivider';
Expand Down Expand Up @@ -127,6 +128,7 @@ export default {
mappers,
},
components: {
Backdrop,
CoachContentLabel,
DownloadButton,
ProgressBar,
Expand Down
70 changes: 70 additions & 0 deletions kolibri/core/assets/src/views/Backdrop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<template>

<transition name="backdrop" appear>
<div
class="backdrop"
:class="{ 'has-transitions': transitions }"
@click="$emit('click')"
>
<slot></slot>
</div>
</transition>

</template>


<script>
export default {
name: 'Backdrop',
props: {
transitions: {
// If true, backdrop will have enter/leave transitions
type: Boolean,
default: false,
},
},
};
</script>


<style lang="scss" scoped>
.backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
background-attachment: fixed;
}
.has-transitions.backdrop-enter {
opacity: 0;
}
.has-transitions.backdrop-enter-to {
opacity: 1;
}
.has-transitions.backdrop-enter-active {
transition: opacity 0.2s ease-in-out;
}
.has-transitions.backdrop-leave {
opacity: 1;
}
.has-transitions.backdrop-leave-to {
opacity: 0;
}
.has-transitions.backdrop-leave-active {
transition: opacity 0.2s ease-in-out;
}
</style>
10 changes: 3 additions & 7 deletions kolibri/core/assets/src/views/CoreSnackbar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<div>
<template v-if="backdrop">
<div class="snackbar-backdrop"></div>
<Backdrop class="snackbar-backdrop" />
<!-- Prevent focus from leaving the this container -->
<div tabindex="0" @focus="trapFocus"></div>
</template>
Expand Down Expand Up @@ -32,11 +32,13 @@
import { mapActions } from 'vuex';
import UiSnackbar from 'kolibri-design-system/lib/keen/UiSnackbar.vue';
import Backdrop from 'kolibri.coreVue.components.Backdrop';
/* Snackbars are used to display notification. */
export default {
name: 'CoreSnackbar',
components: {
Backdrop,
UiSnackbar,
},
props: {
Expand Down Expand Up @@ -150,13 +152,7 @@
}
.snackbar-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 24; // material dialog - ensures we cover KModal
background-color: rgba(0, 0, 0, 0.7);
}
.snackbar-enter-active {
Expand Down
47 changes: 8 additions & 39 deletions kolibri/core/assets/src/views/SideNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,12 @@
</div>
</transition>

<transition name="side-nav-backdrop">
<div
v-show="navShown"
class="side-nav-backdrop"
@click="toggleNav"
>
</div>
</transition>
<Backdrop
v-show="navShown"
:transitions="true"
class="side-nav-backdrop"
@click="toggleNav"
/>

<PrivacyInfoModal
v-if="privacyModalVisible"
Expand All @@ -140,6 +138,7 @@
import navComponents from 'kolibri.utils.navComponents';
import PrivacyInfoModal from 'kolibri.coreVue.components.PrivacyInfoModal';
import branding from 'kolibri.utils.branding';
import Backdrop from 'kolibri.coreVue.components.Backdrop';
import navComponentsMixin from '../mixins/nav-components';
import logout from './LogoutSideNavEntry';
import SideNavDivider from './SideNavDivider';
Expand All @@ -157,6 +156,7 @@
export default {
name: 'SideNav',
components: {
Backdrop,
CoreMenu,
CoreLogo,
SideNavDivider,
Expand Down Expand Up @@ -364,38 +364,7 @@
}
.side-nav-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 15;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
background-attachment: fixed;
}
.side-nav-backdrop-enter {
opacity: 0;
}
.side-nav-backdrop-enter-to {
opacity: 1;
}
.side-nav-backdrop-enter-active {
transition: opacity 0.2s ease-in-out;
}
.side-nav-backdrop-leave {
opacity: 1;
}
.side-nav-backdrop-leave-to {
opacity: 0;
}
.side-nav-backdrop-leave-active {
transition: opacity 0.2s ease-in-out;
}
/* keen menu */
Expand Down
3 changes: 2 additions & 1 deletion kolibri/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"core-js": "^3.2.1",
"csv-generator-client": "^2.1.1",
"date-fns": "^1.28.2",
"fuse.js": "^6.4.6",
"fontfaceobserver": "^2.0.13",
"frame-throttle": "^3.0.0",
"fuse.js": "^6.4.6",
"hammerjs": "^2.0.8",
"intl": "^1.2.4",
"knuth-shuffle-seeded": "^1.0.6",
Expand All @@ -23,6 +23,7 @@
"markdown-it": "^8.3.1",
"screenfull": "^4.0.0",
"shave": "^2.1.3",
"tinycolor2": "^1.4.2",
"vue": "^2.6.11",
"vue-intl": "3.0.0",
"vue-markdown": "^2.1.3",
Expand Down
7 changes: 6 additions & 1 deletion kolibri/plugins/learn/assets/src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,15 @@ export default [
{
name: PageNames.TOPICS_CHANNEL,
path: '/topics/:channel_id',
handler: toRoute => {
handler: (toRoute, fromRoute) => {
if (unassignedContentGuard()) {
return unassignedContentGuard();
}
// If navigation is triggered by a custom channel updating the
// context query param, do not run the handler
if (toRoute.params.channel_id === fromRoute.params.channel_id) {
return;
}
showTopicsChannel(store, toRoute.params.channel_id);
},
},
Expand Down
92 changes: 92 additions & 0 deletions kolibri/plugins/learn/assets/src/utils/__test__/themes.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import logging from 'kolibri.lib.logging';
import { validateTheme } from '../themes';

const defaultTheme = {
appBarColor: null,
textColor: null,
backdropColor: null,
backgroundColor: null,
};

describe('theme validator', () => {
let logError = jest.fn();
let logWarn = jest.fn();

beforeAll(() => {
const logger = logging.getLogger('theme validation');
logError = jest.spyOn(logger, 'error').mockImplementation();
logWarn = jest.spyOn(logger, 'warn').mockImplementation();
});

it('accepts valid colors for the theme', () => {
const validTheme = {
appBarColor: 'rgb(247, 138, 224)',
textColor: '#282a36',
backdropColor: 'lightblue',
backgroundColor: 'rgba(40, 42, 54, 1.00)',
};
const theme = validateTheme(validTheme);
expect(logWarn).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
expect(theme).toEqual({
appBarColor: 'rgb(247, 138, 224)',
textColor: '#282a36',
backdropColor: 'rgba(173, 216, 230, 0.7)', // opacity is added to backdrop color
backgroundColor: 'rgba(40, 42, 54, 1.00)',
});
});

const invalidThemeTests = [
['appBarColor', 'rgb()'],
['textColor', 'purplish'],
['backdropColor', 100],
['backgroundColor', '#YYTTXX'],
];
it.each(invalidThemeTests)('handles invalid setting of %s to %s', (key, color) => {
const expectedLog = `invalid color '${color}' provided for '${key}'`;
const theme = validateTheme({ ...defaultTheme, [key]: color });
expect(logError).toHaveBeenCalledWith(expectedLog);
// No changes are made
expect(theme).toEqual(defaultTheme);
});

it('logs an error if an invalid theme key is provided', () => {
const theme = validateTheme({ fontFamily: 'Roboto Mono' });
expect(logError).toHaveBeenCalledWith(`'fontFamily' is not a valid custom theme option`);
expect(theme).toEqual(defaultTheme);
});

it('logs a warning if the chosen colors do not pass WCAG Level AA', () => {
const theme = validateTheme({
textColor: 'white',
appBarColor: 'rgb(238, 238, 238)', // a light grey
});
expect(logWarn).toHaveBeenCalledWith(
"'textColor' and 'appBarColor' do not provide enough contrast and do not pass WCAG Level AA guidelines"
);
expect(theme).toEqual({
textColor: 'white',
appBarColor: 'rgb(238, 238, 238)', // a light grey
backdropColor: null,
backgroundColor: null,
});
});

const requiredOptionsTests = [
['black', undefined],
[undefined, 'white'],
];
it.each(requiredOptionsTests)(
'logs an error if valid values for both textColor and appBarColor are not provided',
(appBarColor, textColor) => {
const theme = validateTheme({
textColor,
appBarColor,
});
expect(logError).toHaveBeenCalledWith(
`valid values for 'textColor' and 'appBarColor' must be provided`
);
expect(theme).toEqual(defaultTheme);
}
);
});
57 changes: 57 additions & 0 deletions kolibri/plugins/learn/assets/src/utils/themes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import tinycolor from 'tinycolor2';
import logging from 'kolibri.lib.logging';

const logger = logging.getLogger('theme validation');

const validKeys = ['appBarColor', 'textColor', 'backdropColor', 'backgroundColor'];

const defaultTheme = {
appBarColor: null,
textColor: null,
backdropColor: null,
backgroundColor: null,
};

export function validateTheme(theme) {
const updatedTheme = {
...defaultTheme,
};
for (let key in theme) {
const color = theme[key];
if (!color) {
continue;
} else if (!validKeys.includes(key)) {
logger.error(`'${key}' is not a valid custom theme option`);
} else if (!tinycolor(color).isValid()) {
logger.error(`invalid color '${color}' provided for '${key}'`);
} else {
updatedTheme[key] = color;
}
}

// if backdrop color has no transparency, give it an alpha of 0.7
if (updatedTheme.backdropColor) {
const bdcolor = tinycolor(updatedTheme.backdropColor);
bdcolor.setAlpha(0.7);
updatedTheme.backdropColor = bdcolor.toRgbString();
}

// if updated theme does not have valid values for both appBarColor and textColor,
// log an error and reset the missing options to null
if ((theme.textColor && !theme.appBarColor) || (!theme.textColor && theme.appBarColor)) {
logger.error(`valid values for 'textColor' and 'appBarColor' must be provided`);
updatedTheme.textColor = null;
updatedTheme.appBarColor = null;
}

// check contrast and log a warning if it's not WCAG Level AA-compliant
if (updatedTheme.textColor && updatedTheme.appBarColor) {
if (!tinycolor.isReadable(updatedTheme.textColor, updatedTheme.appBarColor)) {
logger.warn(
`'textColor' and 'appBarColor' do not provide enough contrast and do not pass WCAG Level AA guidelines`
);
}
}

return updatedTheme;
}

0 comments on commit 79ca8e4

Please sign in to comment.