Commit 67a51874 authored by Robert Knight's avatar Robert Knight

Separate out icon definitions from `SvgIcon` component

Replace the hardcoded set of available icons in `svg-icon.js` with a
`registerIcons` function which is called by the application at startup
to register the icons that should be available for use.

This will enable us to re-use the `SvgIcon` component in the "annotator"
part of the client or other applications (as part of our UI component
library) which use a different set of icons.

The lms application currently has a different solution for this in its
`SvgIcon` implementation which requires the icon markup to be passed
each time the component is used. On reflection, I think that passing
just the name at the point of use is going to be more convenient in app
code and when writing tests.  It also avoids the need to validate the
markup is from a trusted source on each use.
parent f593f98b
......@@ -3,54 +3,10 @@ import { createElement } from 'preact';
import { useLayoutEffect, useRef } from 'preact/hooks';
import propTypes from 'prop-types';
// The list of supported icons
const icons = {
add: require('../../images/icons/add.svg'),
annotate: require('../../images/icons/annotate.svg'),
'arrow-left': require('../../images/icons/arrow-left.svg'),
'arrow-right': require('../../images/icons/arrow-right.svg'),
cancel: require('../../images/icons/cancel.svg'),
'caret-right': require('../../images/icons/caret-right.svg'),
'cc-std': require('../../images/icons/cc-std.svg'),
'cc-zero': require('../../images/icons/cc-zero.svg'),
'collapse-menu': require('../../images/icons/collapse-menu.svg'),
copy: require('../../images/icons/copy.svg'),
cursor: require('../../images/icons/cursor.svg'),
edit: require('../../images/icons/edit.svg'),
email: require('../../images/icons/email.svg'),
'expand-menu': require('../../images/icons/expand-menu.svg'),
error: require('../../images/icons/cancel.svg'),
external: require('../../images/icons/external.svg'),
facebook: require('../../images/icons/facebook.svg'),
flag: require('../../images/icons/flag.svg'),
'flag--active': require('../../images/icons/flag--active.svg'),
'format-bold': require('../../images/icons/format-bold.svg'),
'format-functions': require('../../images/icons/format-functions.svg'),
'format-italic': require('../../images/icons/format-italic.svg'),
'format-list-numbered': require('../../images/icons/format-list-numbered.svg'),
'format-list-unordered': require('../../images/icons/format-list-unordered.svg'),
'format-quote': require('../../images/icons/format-quote.svg'),
groups: require('../../images/icons/groups.svg'),
help: require('../../images/icons/help.svg'),
highlight: require('../../images/icons/highlight.svg'),
image: require('../../images/icons/image.svg'),
leave: require('../../images/icons/leave.svg'),
link: require('../../images/icons/link.svg'),
lock: require('../../images/icons/lock.svg'),
logo: require('../../images/icons/logo.svg'),
pointer: require('../../images/icons/pointer.svg'),
profile: require('../../images/icons/profile.svg'),
public: require('../../images/icons/public.svg'),
refresh: require('../../images/icons/refresh.svg'),
restricted: require('../../images/icons/restricted.svg'),
reply: require('../../images/icons/reply.svg'),
search: require('../../images/icons/search.svg'),
share: require('../../images/icons/share.svg'),
success: require('../../images/icons/check.svg'),
sort: require('../../images/icons/sort.svg'),
trash: require('../../images/icons/trash.svg'),
twitter: require('../../images/icons/twitter.svg'),
};
/**
* Map of icon name to SVG data.
*/
let iconRegistry = {};
/**
* Component that renders icons using inline `<svg>` elements.
......@@ -65,10 +21,10 @@ export default function SvgIcon({
inline = false,
title = '',
}) {
if (!icons[name]) {
throw new Error(`Unknown icon ${name}`);
if (!iconRegistry[name]) {
throw new Error(`Icon name "${name}" is not registered`);
}
const markup = { __html: icons[name] };
const markup = { __html: iconRegistry[name] };
const element = useRef();
useLayoutEffect(() => {
......@@ -97,7 +53,12 @@ export default function SvgIcon({
}
SvgIcon.propTypes = {
/** The name of the icon to load. */
/**
* The name of the icon to load.
*
* The name must match a name that has already been registered using the
* `registerIcons` function.
*/
name: propTypes.string,
/** A CSS class to apply to the `<svg>` element. */
......@@ -109,3 +70,26 @@ SvgIcon.propTypes = {
/** Optional title attribute to apply to the SVG's containing `span` */
title: propTypes.string,
};
/**
* Register icons for use with the `SvgIcon` component.
*
* @param {Object} icons - Object mapping icon names to SVG data.
* @param {boolean} [options.reset] - If `true`, remove existing registered icons.
*/
export function registerIcons(icons, { reset = false } = {}) {
if (reset) {
iconRegistry = {};
}
Object.assign(iconRegistry, icons);
}
/**
* Return the currently available icons.
*
* To register icons, don't mutate this directly but call `registerIcons`
* instead.
*/
export function availableIcons() {
return iconRegistry;
}
import { createElement, render } from 'preact';
import SvgIcon from '../svg-icon';
import SvgIcon, { availableIcons, registerIcons } from '../svg-icon';
describe('SvgIcon', () => {
// Tests here use DOM APIs rather than Enzyme because SvgIcon uses
// `dangerouslySetInnerHTML` for its content, and that is not visible in the
// Enzyme tree.
// Global icon set that is registered with `SvgIcon` outside of these tests.
let savedIconSet;
beforeEach(() => {
savedIconSet = availableIcons();
registerIcons(
{
'collapse-menu': require('../../../images/icons/collapse-menu.svg'),
'expand-menu': require('../../../images/icons/expand-menu.svg'),
refresh: require('../../../images/icons/refresh.svg'),
},
{ reset: true }
);
});
afterEach(() => {
registerIcons(savedIconSet, { reset: true });
});
it("sets the element's content to the content of the SVG", () => {
const container = document.createElement('div');
render(<SvgIcon name="refresh" />, container);
assert.ok(container.querySelector('svg'));
});
it('throws an error if the icon is unknown', () => {
it('throws an error if the icon name is not registered', () => {
assert.throws(() => {
const container = document.createElement('div');
render(<SvgIcon name="unknown" />, container);
});
}, 'Icon name "unknown" is not registered');
});
it('does not set the class of the SVG by default', () => {
......
/**
* Set of icons used by the sidebar application for use with the `SvgIcon`
* component.
*/
export default {
add: require('../images/icons/add.svg'),
annotate: require('../images/icons/annotate.svg'),
'arrow-left': require('../images/icons/arrow-left.svg'),
'arrow-right': require('../images/icons/arrow-right.svg'),
cancel: require('../images/icons/cancel.svg'),
'caret-right': require('../images/icons/caret-right.svg'),
'cc-std': require('../images/icons/cc-std.svg'),
'cc-zero': require('../images/icons/cc-zero.svg'),
'collapse-menu': require('../images/icons/collapse-menu.svg'),
copy: require('../images/icons/copy.svg'),
cursor: require('../images/icons/cursor.svg'),
edit: require('../images/icons/edit.svg'),
email: require('../images/icons/email.svg'),
'expand-menu': require('../images/icons/expand-menu.svg'),
error: require('../images/icons/cancel.svg'),
external: require('../images/icons/external.svg'),
facebook: require('../images/icons/facebook.svg'),
flag: require('../images/icons/flag.svg'),
'flag--active': require('../images/icons/flag--active.svg'),
'format-bold': require('../images/icons/format-bold.svg'),
'format-functions': require('../images/icons/format-functions.svg'),
'format-italic': require('../images/icons/format-italic.svg'),
'format-list-numbered': require('../images/icons/format-list-numbered.svg'),
'format-list-unordered': require('../images/icons/format-list-unordered.svg'),
'format-quote': require('../images/icons/format-quote.svg'),
groups: require('../images/icons/groups.svg'),
help: require('../images/icons/help.svg'),
highlight: require('../images/icons/highlight.svg'),
image: require('../images/icons/image.svg'),
leave: require('../images/icons/leave.svg'),
link: require('../images/icons/link.svg'),
lock: require('../images/icons/lock.svg'),
logo: require('../images/icons/logo.svg'),
pointer: require('../images/icons/pointer.svg'),
profile: require('../images/icons/profile.svg'),
public: require('../images/icons/public.svg'),
refresh: require('../images/icons/refresh.svg'),
restricted: require('../images/icons/restricted.svg'),
reply: require('../images/icons/reply.svg'),
search: require('../images/icons/search.svg'),
share: require('../images/icons/share.svg'),
success: require('../images/icons/check.svg'),
sort: require('../images/icons/sort.svg'),
trash: require('../images/icons/trash.svg'),
twitter: require('../images/icons/twitter.svg'),
};
......@@ -107,6 +107,11 @@ function autosave(autosaveService) {
autosaveService.init();
}
// Register icons used by the sidebar app (and maybe other assets in future).
import { registerIcons } from './components/svg-icon';
import iconSet from './icons';
registerIcons(iconSet);
// Preact UI components that are wrapped for use within Angular templates.
import Annotation from './components/annotation';
......
......@@ -23,3 +23,10 @@ import { configure } from 'enzyme';
import { Adapter } from 'enzyme-adapter-preact-pure';
configure({ adapter: new Adapter() });
// Make all the icons that are available for use with `SvgIcon` in the actual
// app available in the tests. This enables validation of icon names passed to
// `SvgIcon`.
import iconSet from '../icons';
import { registerIcons } from '../components/svg-icon';
registerIcons(iconSet);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment