Commit 63359995 authored by Kyle Keating's avatar Kyle Keating Committed by Kyle Keating

Switch all SvgIcon instances to frontend-shared version

parent 34062d5d
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import { useShortcut } from '../../shared/shortcut';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @param {Object} props
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import propTypes from 'prop-types';
import { createElement } from 'preact';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @param {Object} props
* @param {import("preact").Ref<HTMLButtonElement>} [props.buttonRef]
......
......@@ -13,7 +13,7 @@ if (process.env.NODE_ENV !== 'production') {
}
// Load icons.
import { registerIcons } from '../shared/components/svg-icon';
import { registerIcons } from '@hypothesis/frontend-shared';
import iconSet from './icons';
registerIcons(iconSet);
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import debounce from 'lodash.debounce';
import { Fragment, createElement, render } from 'preact';
......@@ -6,7 +7,6 @@ import Delegator from '../delegator';
import RenderingStates from '../pdfjs-rendering-states';
import PDFMetadata from './pdf-metadata';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../../types/annotator').Anchor} Anchor
......
import classnames from 'classnames';
import { createElement } from 'preact';
import { useLayoutEffect, useRef } from 'preact/hooks';
import propTypes from 'prop-types';
/**
* Object mapping icon names to SVG markup.
*
* @typedef {Object.<string,string>} IconMap
*/
/**
* @template T
* @typedef {import("preact/hooks").Ref<T>} Ref
*/
/**
* Map of icon name to SVG data.
*
* @type {IconMap}
*/
let iconRegistry = {};
/**
* @typedef SvgIconProps
* @prop {string} name - The name of the icon to display.
* The name must match a name that has already been registered using the
* `registerIcons` function.
* @prop {string} [className] - A CSS class to apply to the `<svg>` element.
* @prop {boolean} [inline] - Apply a style allowing for inline display of icon wrapper.
* @prop {string} [title] - Optional title attribute to apply to the SVG's containing `span`.
*/
/**
* Component that renders icons using inline `<svg>` elements.
* This enables their appearance to be customized via CSS.
*
* This matches the way we do icons on the website, see
* https://github.com/hypothesis/h/pull/3675
*
* @param {SvgIconProps} props
*/
export default function SvgIcon({
name,
className = '',
inline = false,
title = '',
}) {
if (!iconRegistry[name]) {
throw new Error(`Icon name "${name}" is not registered`);
}
const markup = iconRegistry[name];
const element = /** @type {Ref<HTMLElement>} */ (useRef());
useLayoutEffect(() => {
const svg = element.current.querySelector('svg');
// The icon should always contain an `<svg>` element, but check here as we
// don't validate the markup when it is registered.
if (svg) {
svg.setAttribute('class', className);
}
}, [
className,
// `markup` is a dependency of this effect because the SVG is replaced if
// it changes.
markup,
]);
const spanProps = {};
if (title) {
spanProps.title = title;
}
return (
<span
className={classnames('svg-icon', { 'svg-icon--inline': inline })}
dangerouslySetInnerHTML={{ __html: markup }}
ref={element}
{...spanProps}
/>
);
}
SvgIcon.propTypes = {
name: propTypes.string.isRequired,
className: propTypes.string,
inline: propTypes.bool,
title: propTypes.string,
};
/**
* Register icons for use with the `SvgIcon` component.
*
* @param {IconMap} icons
* @param {Object} options
* @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.
*
* @return {IconMap}
*/
export function availableIcons() {
return iconRegistry;
}
import { createElement, render } from 'preact';
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 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', () => {
const container = document.createElement('div');
render(<SvgIcon name="refresh" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), '');
});
it('sets the class of the SVG if provided', () => {
const container = document.createElement('div');
render(<SvgIcon name="refresh" className="thing__icon" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), 'thing__icon');
});
it('retains the CSS class if the icon changes', () => {
const container = document.createElement('div');
render(<SvgIcon name="expand-menu" className="thing__icon" />, container);
render(<SvgIcon name="collapse-menu" className="thing__icon" />, container);
const svg = container.querySelector('svg');
assert.equal(svg.getAttribute('class'), 'thing__icon');
});
it('sets a default class on the wrapper element', () => {
const container = document.createElement('div');
render(<SvgIcon name="expand-menu" />, container);
const wrapper = container.querySelector('span');
assert.isTrue(wrapper.classList.contains('svg-icon'));
assert.isFalse(wrapper.classList.contains('svg-icon--inline'));
});
it('appends an inline class to wrapper if `inline` prop is `true`', () => {
const container = document.createElement('div');
render(<SvgIcon name="expand-menu" inline={true} />, container);
const wrapper = container.querySelector('span');
assert.isTrue(wrapper.classList.contains('svg-icon'));
assert.isTrue(wrapper.classList.contains('svg-icon--inline'));
});
it('sets a title to the containing `span` element if `title` is present', () => {
const container = document.createElement('div');
render(<SvgIcon name="expand-menu" title="Open menu" />, container);
const wrapper = container.querySelector('span');
assert.equal(wrapper.getAttribute('title'), 'Open menu');
});
it('sets does not set a title on the containing `span` element if `title` not present', () => {
const container = document.createElement('div');
render(<SvgIcon name="expand-menu" />, container);
const wrapper = container.querySelector('span');
assert.notOk(wrapper.getAttribute('title'));
});
});
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import { useMemo } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -14,7 +15,6 @@ import AnnotationDocumentInfo from './annotation-document-info';
import AnnotationShareInfo from './annotation-share-info';
import AnnotationUser from './annotation-user';
import Button from './button';
import SvgIcon from '../../shared/components/svg-icon';
import AnnotationTimestamps from './annotation-timestamps';
/**
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import SvgIcon from '../../shared/components/svg-icon';
/**
* Render information about CC licensing
*/
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
......@@ -10,7 +11,6 @@ import { applyTheme } from '../helpers/theme';
import Button from './button';
import Menu from './menu';
import MenuItem from './menu-item';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../../types/api').Annotation} Annotation
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -10,7 +11,6 @@ import { withServices } from '../service-context';
import Button from './button';
import useElementShouldClose from './hooks/use-element-should-close';
import ShareLinks from './share-links';
import SvgIcon from '../../shared/components/svg-icon';
import { isIOS } from '../../shared/user-agent';
/**
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store';
import { isPrivate } from '../helpers/permissions';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import("../../types/api").Annotation} Annotation
* @typedef {import('../../types/api').Group} Group
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef ButtonProps
* @prop {string} [buttonText] -
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import Menu from './menu';
import MenuItem from './menu-item';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../store/modules/filters').FilterOption} FilterOption
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import { withServices } from '../service-context';
import Button from './button';
import SvgIcon from '../../shared/components/svg-icon';
/** @typedef {import('../services/service-url').ServiceUrlGetter} ServiceUrlGetter */
......
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement, createRef } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -13,7 +14,6 @@ import { normalizeKeyName } from '../../shared/browser-compatibility-utils';
import { isMacOS } from '../../shared/user-agent';
import MarkdownView from './markdown-view';
import SvgIcon from '../../shared/components/svg-icon';
// Mapping of toolbar command name to key for Ctrl+<key> keyboard shortcuts.
// The shortcuts are taken from Stack Overflow's editor.
......
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { Fragment, createElement } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import propTypes from 'prop-types';
import { normalizeKeyName } from '../../shared/browser-compatibility-utils';
import SvgIcon from '../../shared/components/svg-icon';
import MenuKeyboardNavigation from './menu-keyboard-navigation';
import Slider from './slider';
......
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { Fragment, createElement } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -6,7 +7,6 @@ import propTypes from 'prop-types';
import useElementShouldClose from './hooks/use-element-should-close';
import { normalizeKeyName } from '../../shared/browser-compatibility-utils';
import SvgIcon from '../../shared/components/svg-icon';
import MenuKeyboardNavigation from './menu-keyboard-navigation';
// The triangular indicator below the menu toggle button that visually links it
......
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
......@@ -7,7 +8,6 @@ import uiConstants from '../ui-constants';
import { withServices } from '../service-context';
import NewNoteBtn from './new-note-btn';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../../types/config').MergedConfig} MergedConfig
......
import { createElement, Fragment } from 'preact';
import { SvgIcon } from '@hypothesis/frontend-shared';
import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store';
......@@ -12,7 +13,6 @@ import Button from './button';
import ShareLinks from './share-links';
import SidebarPanel from './sidebar-panel';
import Spinner from './spinner';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef ShareAnnotationsPanelProps
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import { withServices } from '../service-context';
import SvgIcon from '../../shared/components/svg-icon';
/**
* A single sharing link as a list item
*/
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import classnames from 'classnames';
import propTypes from 'prop-types';
......@@ -5,7 +6,6 @@ import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store';
import Button from './button';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef SidebarContentErrorProps
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -7,7 +8,6 @@ import { useStoreProxy } from '../store/use-store';
import Button from './button';
import Slider from './slider';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef SidebarPanelProps
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import { useStoreProxy } from '../store/use-store';
import Menu from './menu';
import MenuItem from './menu-item';
import SvgIcon from '../../shared/components/svg-icon';
/**
* A drop-down menu of sorting options for a collection of annotations.
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import { useRef, useState } from 'preact/hooks';
import propTypes from 'prop-types';
......@@ -6,7 +7,6 @@ import { withServices } from '../service-context';
import AutocompleteList from './autocomplete-list';
import { normalizeKeyName } from '../../shared/browser-compatibility-utils';
import SvgIcon from '../../shared/components/svg-icon';
import useElementShouldClose from './hooks/use-element-should-close';
/** @typedef {import("preact").JSX.Element} JSXElement */
......
import classnames from 'classnames';
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import { useStoreProxy } from '../store/use-store';
import { withServices } from '../service-context';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../store/modules/toast-messages').ToastMessage} ToastMessage
*/
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
import isThirdPartyService from '../helpers/is-third-party-service';
import { withServices } from '../service-context';
import SvgIcon from '../../shared/components/svg-icon';
/**
* Subcomponent: an "instruction" within the tutorial step that includes an
* icon and a "command" associated with that icon. Encapsulating these together
......
import { SvgIcon } from '@hypothesis/frontend-shared';
import { createElement } from 'preact';
import propTypes from 'prop-types';
......@@ -10,7 +11,6 @@ import { withServices } from '../service-context';
import Menu from './menu';
import MenuItem from './menu-item';
import MenuSection from './menu-section';
import SvgIcon from '../../shared/components/svg-icon';
/**
* @typedef {import('../services/service-url').ServiceUrlGetter} ServiceUrlGetter
......
......@@ -93,11 +93,9 @@ function setupFrameSync(frameSync, store) {
}
// Register icons used by the sidebar app (and maybe other assets in future).
import { registerIcons as registerIconsDeprecated } from '../shared/components/svg-icon';
import { registerIcons } from '@hypothesis/frontend-shared';
import iconSet from './icons';
registerIconsDeprecated(iconSet); // Temporary until we replace this module
registerIcons(iconSet);
// The entry point component for the app.
......
......@@ -18,7 +18,7 @@ configure({ adapter: new Adapter() });
// `SvgIcon`.
import sidebarIcons from '../icons';
import annotatorIcons from '../../annotator/icons';
import { registerIcons } from '../../shared/components/svg-icon';
import { registerIcons } from '@hypothesis/frontend-shared';
registerIcons({
...sidebarIcons,
...annotatorIcons,
......
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