Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ContentModal to display resources within the context of a Custom Channel Renderer #8040

Merged
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;
}