diff --git a/.eslintrc.js b/.eslintrc.js index f9dbc71df..9a92e50a8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,6 +94,12 @@ module.exports = { 'no-loop-func': 'error', 'no-mixed-spaces-and-tabs': 'error', 'no-nested-ternary': 'warn', + 'no-restricted-imports': ['error', { + patterns: [{ + group: ['react-inlinesvg'], + message: 'Use the SvgIcon component instead.', + }], + }], 'no-trailing-spaces': 'warn', 'no-undef': 'error', 'no-unreachable': 'error', diff --git a/app/soapbox/components/__mocks__/react-inlinesvg.js b/app/soapbox/components/__mocks__/react-inlinesvg.js deleted file mode 100644 index b63d1a967..000000000 --- a/app/soapbox/components/__mocks__/react-inlinesvg.js +++ /dev/null @@ -1,10 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -export default function InlineSVG({ src }) { - return ; -} - -InlineSVG.propTypes = { - src: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, -}; diff --git a/app/soapbox/components/__mocks__/react-inlinesvg.tsx b/app/soapbox/components/__mocks__/react-inlinesvg.tsx new file mode 100644 index 000000000..367ec0e33 --- /dev/null +++ b/app/soapbox/components/__mocks__/react-inlinesvg.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +interface IInlineSVG { + loader?: JSX.Element, +} + +const InlineSVG: React.FC = ({ loader }): JSX.Element => { + if (loader) { + return loader; + } else { + throw 'You used react-inlinesvg without a loader! This will cause jumpy loading during render.'; + } +}; + +export default InlineSVG; diff --git a/app/soapbox/components/sidebar-navigation-link.tsx b/app/soapbox/components/sidebar-navigation-link.tsx index 4d031d5fd..bd0567c03 100644 --- a/app/soapbox/components/sidebar-navigation-link.tsx +++ b/app/soapbox/components/sidebar-navigation-link.tsx @@ -37,16 +37,14 @@ const SidebarNavigationLink = ({ icon, text, to, count }: ISidebarNavigationLink ) : null} -
- -
+ {text} diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 3d069d10e..0898279f9 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -33,7 +33,7 @@ const SidebarNavigation = () => { {account && ( <> } /> @@ -79,7 +79,7 @@ const SidebarNavigation = () => { /> )} */} - {(account && instance.get('invites_enabled')) && ( + {(account && instance.invites_enabled) && ( { src={require('@tabler/icons/icons/users.svg')} className={classNames('primary-navigation__icon', { 'svg-icon--active': location.pathname === '/timeline/local' })} /> - {instance.get('title')} + {instance.title} ) : ( diff --git a/app/soapbox/components/status-action-button.tsx b/app/soapbox/components/status-action-button.tsx index 8095e4c7a..032563315 100644 --- a/app/soapbox/components/status-action-button.tsx +++ b/app/soapbox/components/status-action-button.tsx @@ -1,8 +1,7 @@ import classNames from 'classnames'; import React from 'react'; -import InlineSVG from 'react-inlinesvg'; -import { Text } from 'soapbox/components/ui'; +import { Text, Icon } from 'soapbox/components/ui'; import { shortNumberFormat } from 'soapbox/utils/numbers'; const COLORS = { @@ -53,10 +52,10 @@ const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: Re )} {...filteredProps} > - - + } /> ); } diff --git a/app/soapbox/components/ui/card/card.tsx b/app/soapbox/components/ui/card/card.tsx index 8c69b3bd9..412c1985b 100644 --- a/app/soapbox/components/ui/card/card.tsx +++ b/app/soapbox/components/ui/card/card.tsx @@ -1,10 +1,10 @@ import classNames from 'classnames'; import React from 'react'; -import InlineSVG from 'react-inlinesvg'; import { defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { Text } from 'soapbox/components/ui'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; const sizes = { md: 'p-4 sm:rounded-xl', @@ -54,7 +54,7 @@ const CardHeader: React.FC = ({ children, backHref, onBackClick }): return ( - + Back ); diff --git a/app/soapbox/components/ui/icon-button/icon-button.tsx b/app/soapbox/components/ui/icon-button/icon-button.tsx index 10d08fc80..319fa8847 100644 --- a/app/soapbox/components/ui/icon-button/icon-button.tsx +++ b/app/soapbox/components/ui/icon-button/icon-button.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React from 'react'; -import InlineSVG from 'react-inlinesvg'; +import SvgIcon from '../icon/svg-icon'; import Text from '../text/text'; interface IIconButton extends React.ButtonHTMLAttributes { @@ -24,7 +24,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef {...filteredProps} data-testid='icon-button' > - + {text ? ( diff --git a/app/soapbox/components/ui/icon/__tests__/svg-icon.test.tsx b/app/soapbox/components/ui/icon/__tests__/svg-icon.test.tsx new file mode 100644 index 000000000..a1e269dd8 --- /dev/null +++ b/app/soapbox/components/ui/icon/__tests__/svg-icon.test.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +import { render, screen } from '../../../../jest/test-helpers'; +import SvgIcon from '../svg-icon'; + +describe('', () => { + it('renders loading element with default size', () => { + render(); + + const svg = screen.getByTestId('svg-icon-loader'); + expect(svg.getAttribute('width')).toBe('24'); + expect(svg.getAttribute('height')).toBe('24'); + expect(svg.getAttribute('class')).toBe('text-primary-500'); + }); +}); diff --git a/app/soapbox/components/ui/icon/icon.tsx b/app/soapbox/components/ui/icon/icon.tsx index a9ea24377..e8d8162a9 100644 --- a/app/soapbox/components/ui/icon/icon.tsx +++ b/app/soapbox/components/ui/icon/icon.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import InlineSVG from 'react-inlinesvg'; + +import SvgIcon from './svg-icon'; interface IIcon { className?: string, count?: number, alt?: string, src: string, + size?: number, } -const Icon = ({ src, alt, count, ...filteredProps }: IIcon): JSX.Element => ( +const Icon = ({ src, alt, count, size, ...filteredProps }: IIcon): JSX.Element => (
{count ? ( @@ -16,7 +18,7 @@ const Icon = ({ src, alt, count, ...filteredProps }: IIcon): JSX.Element => ( ) : null} - +
); diff --git a/app/soapbox/components/ui/icon/svg-icon.tsx b/app/soapbox/components/ui/icon/svg-icon.tsx new file mode 100644 index 000000000..5cb2dd192 --- /dev/null +++ b/app/soapbox/components/ui/icon/svg-icon.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports + +interface ISvgIcon { + className?: string, + alt?: string, + src: string, + size?: number, +} + +/** Renders an inline SVG with an empty frame loading state */ +const SvgIcon: React.FC = ({ src, alt, size = 24, className }): JSX.Element => { + const loader = ( + + ); + + return ( + + /* If the fetch fails, fall back to displaying the loader */ + {loader} + + ); +}; + +export default SvgIcon; diff --git a/app/soapbox/components/ui/input/input.tsx b/app/soapbox/components/ui/input/input.tsx index 282658ad0..343a9d0dd 100644 --- a/app/soapbox/components/ui/input/input.tsx +++ b/app/soapbox/components/ui/input/input.tsx @@ -1,9 +1,9 @@ import classNames from 'classnames'; import React from 'react'; -import InlineSVG from 'react-inlinesvg'; import { defineMessages, useIntl } from 'react-intl'; import Icon from '../icon/icon'; +import SvgIcon from '../icon/svg-icon'; import Tooltip from '../tooltip/tooltip'; const messages = defineMessages({ @@ -72,7 +72,7 @@ const Input = React.forwardRef( tabIndex={-1} className='text-gray-400 hover:text-gray-500 h-full px-2 focus:ring-primary-500 focus:ring-2' > - diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 9ccbd204f..bf60de896 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import InlineSVG from 'react-inlinesvg'; import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; @@ -16,6 +15,7 @@ import Avatar from 'soapbox/components/avatar'; import Badge from 'soapbox/components/badge'; import StillImage from 'soapbox/components/still_image'; import { HStack, IconButton, Menu, MenuButton, MenuItem, MenuList, MenuLink, MenuDivider } from 'soapbox/components/ui'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import ActionButton from 'soapbox/features/ui/components/action_button'; import { isLocal, @@ -614,7 +614,7 @@ class Header extends ImmutablePureComponent { return (
- +
{menuItem.text}
diff --git a/app/soapbox/features/compose/components/search.tsx b/app/soapbox/features/compose/components/search.tsx index efe1a6fc1..660045694 100644 --- a/app/soapbox/features/compose/components/search.tsx +++ b/app/soapbox/features/compose/components/search.tsx @@ -2,12 +2,12 @@ import classNames from 'classnames'; import { Map as ImmutableMap } from 'immutable'; import debounce from 'lodash/debounce'; import * as React from 'react'; -import InlineSVG from 'react-inlinesvg'; import { defineMessages, useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import AutosuggestAccountInput from 'soapbox/components/autosuggest_account_input'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import { useAppSelector } from 'soapbox/hooks'; import { @@ -140,12 +140,12 @@ const Search = (props: ISearch) => { className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer' onClick={handleClear} > - - {
- + @@ -40,7 +40,7 @@ const Developers = () => { - + @@ -48,7 +48,7 @@ const Developers = () => { - + @@ -56,7 +56,7 @@ const Developers = () => {