This commit is contained in:
209
react/features/base/icons/components/Icon.tsx
Normal file
209
react/features/base/icons/components/Icon.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Container } from '../../react/components/index';
|
||||
import { StyleType, styleTypeToObject } from '../../styles/functions';
|
||||
|
||||
import { IIconProps } from './types';
|
||||
|
||||
interface IProps extends IIconProps {
|
||||
|
||||
/**
|
||||
* Optional label for screen reader users.
|
||||
*
|
||||
* If set, this is will add a `aria-label` attribute on the svg element,
|
||||
* contrary to the aria* props which set attributes on the container element.
|
||||
*
|
||||
* Use this if the icon conveys meaning and is not clickable.
|
||||
*/
|
||||
alt?: string;
|
||||
|
||||
/**
|
||||
* The id of the element this button icon controls.
|
||||
*/
|
||||
ariaControls?: string;
|
||||
|
||||
/**
|
||||
* Id of description label.
|
||||
*/
|
||||
ariaDescribedBy?: string;
|
||||
|
||||
/**
|
||||
* Aria disabled flag for the Icon.
|
||||
*/
|
||||
ariaDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the element popup is expanded.
|
||||
*/
|
||||
ariaExpanded?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the element has a popup.
|
||||
*/
|
||||
ariaHasPopup?: boolean;
|
||||
|
||||
/**
|
||||
* Aria label for the Icon.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
|
||||
/**
|
||||
* Whether the element has a pressed.
|
||||
*/
|
||||
ariaPressed?: boolean;
|
||||
|
||||
/**
|
||||
* Class name for the web platform, if any.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Color of the icon (if not provided by the style object).
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* Id of the icon container.
|
||||
*/
|
||||
containerId?: string;
|
||||
|
||||
/**
|
||||
* Id prop (mainly for autotests).
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* On click handler.
|
||||
*/
|
||||
onClick?: (e?: any) => void;
|
||||
|
||||
/**
|
||||
* Keydown handler.
|
||||
*/
|
||||
onKeyDown?: Function;
|
||||
|
||||
/**
|
||||
* Keypress handler.
|
||||
*/
|
||||
onKeyPress?: Function;
|
||||
|
||||
/**
|
||||
* Role for the Icon.
|
||||
*/
|
||||
role?: string;
|
||||
|
||||
/**
|
||||
* The size of the icon (if not provided by the style object).
|
||||
*/
|
||||
size?: number | string;
|
||||
|
||||
/**
|
||||
* The preloaded icon component to render.
|
||||
*/
|
||||
src: Function;
|
||||
|
||||
/**
|
||||
* Style object to be applied.
|
||||
*/
|
||||
style?: StyleType | StyleType[];
|
||||
|
||||
/**
|
||||
* TabIndex for the Icon.
|
||||
*/
|
||||
tabIndex?: number;
|
||||
|
||||
/**
|
||||
* Test id for the icon.
|
||||
*/
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_COLOR = navigator.product === 'ReactNative' ? 'white' : undefined;
|
||||
export const DEFAULT_SIZE = navigator.product === 'ReactNative' ? 36 : 22;
|
||||
|
||||
/**
|
||||
* Implements an Icon component that takes a loaded SVG file as prop and renders it as an icon.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function Icon(props: IProps) {
|
||||
const {
|
||||
alt,
|
||||
className = '',
|
||||
color,
|
||||
id,
|
||||
containerId,
|
||||
onClick,
|
||||
size,
|
||||
src: IconComponent,
|
||||
style,
|
||||
ariaHasPopup,
|
||||
ariaLabel,
|
||||
ariaDisabled,
|
||||
ariaExpanded,
|
||||
ariaControls,
|
||||
tabIndex,
|
||||
ariaPressed,
|
||||
ariaDescribedBy,
|
||||
role,
|
||||
onKeyPress,
|
||||
onKeyDown,
|
||||
testId,
|
||||
...rest
|
||||
}: IProps = props;
|
||||
|
||||
const {
|
||||
color: styleColor,
|
||||
fontSize: styleSize,
|
||||
...restStyle
|
||||
} = styleTypeToObject(style ?? {});
|
||||
const calculatedColor = color ?? styleColor ?? DEFAULT_COLOR;
|
||||
const calculatedSize = size ?? styleSize ?? DEFAULT_SIZE;
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && onClick) {
|
||||
e.preventDefault();
|
||||
onClick(e);
|
||||
} else if (onKeyPress) {
|
||||
onKeyPress(e);
|
||||
}
|
||||
}, [ onClick, onKeyPress ]);
|
||||
|
||||
const jitsiIconClassName = calculatedColor ? 'jitsi-icon' : 'jitsi-icon jitsi-icon-default';
|
||||
|
||||
const iconProps = alt ? {
|
||||
'aria-label': alt,
|
||||
role: 'img'
|
||||
} : {
|
||||
'aria-hidden': true
|
||||
};
|
||||
|
||||
return (
|
||||
<Container
|
||||
{ ...rest }
|
||||
aria-controls = { ariaControls }
|
||||
aria-describedby = { ariaDescribedBy }
|
||||
aria-disabled = { ariaDisabled }
|
||||
aria-expanded = { ariaExpanded }
|
||||
aria-haspopup = { ariaHasPopup }
|
||||
aria-label = { ariaLabel }
|
||||
aria-pressed = { ariaPressed }
|
||||
className = { `${jitsiIconClassName} ${className || ''}` }
|
||||
data-testid = { testId }
|
||||
id = { containerId }
|
||||
onClick = { onClick }
|
||||
onKeyDown = { onKeyDown }
|
||||
onKeyPress = { onKeyPressHandler }
|
||||
role = { role }
|
||||
style = { restStyle }
|
||||
tabIndex = { tabIndex }>
|
||||
<IconComponent
|
||||
{ ...iconProps }
|
||||
fill = { calculatedColor }
|
||||
height = { calculatedSize }
|
||||
id = { id }
|
||||
width = { calculatedSize } />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
17
react/features/base/icons/components/SvgXmlIcon.native.tsx
Normal file
17
react/features/base/icons/components/SvgXmlIcon.native.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { SvgFromXml } from 'react-native-svg';
|
||||
|
||||
/**
|
||||
* SVG rendering component.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SvgXmlIcon = ({ src, ...rest }: {
|
||||
src: string;
|
||||
}): JSX.Element => (
|
||||
<SvgFromXml
|
||||
override = { rest }
|
||||
xml = { src } />
|
||||
);
|
||||
|
||||
export default SvgXmlIcon;
|
||||
26
react/features/base/icons/components/SvgXmlIcon.web.tsx
Normal file
26
react/features/base/icons/components/SvgXmlIcon.web.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* SVG rendering component.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SvgXmlIcon = ({ src, ...rest }: {
|
||||
src: string;
|
||||
}): JSX.Element => {
|
||||
const svgDocument = new DOMParser().parseFromString(src, 'image/svg+xml');
|
||||
const element = svgDocument.documentElement.outerHTML;
|
||||
const attributes = useMemo(() => Object.entries(rest).map(
|
||||
([ key, value ]) => `${key}="${value}"`)
|
||||
.join(' '), [ rest ]);
|
||||
|
||||
const html = element.replace('<svg', `<svg ${attributes}`);
|
||||
|
||||
return (
|
||||
<div // eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML = {{ __html: html }}
|
||||
{ ...rest } />
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgXmlIcon;
|
||||
5
react/features/base/icons/components/types.native.ts
Normal file
5
react/features/base/icons/components/types.native.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GestureResponderEvent } from 'react-native';
|
||||
|
||||
export interface IIconProps {
|
||||
onClick?: (e?: GestureResponderEvent) => void;
|
||||
}
|
||||
5
react/features/base/icons/components/types.web.ts
Normal file
5
react/features/base/icons/components/types.web.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface IIconProps {
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
}
|
||||
32
react/features/base/icons/components/withBranding.tsx
Normal file
32
react/features/base/icons/components/withBranding.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
|
||||
import SvgXmlIcon from './SvgXmlIcon';
|
||||
|
||||
/**
|
||||
* Icon wrapper that checks for branding before returning the SVG component.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const withBranding = ({ DefaultIcon, iconName }: {
|
||||
DefaultIcon: any;
|
||||
iconName: string;
|
||||
}) => (props: any) => {
|
||||
const src = useSelector((state: IReduxState) =>
|
||||
state['features/dynamic-branding']?.brandedIcons?.[iconName]
|
||||
);
|
||||
|
||||
if (src) {
|
||||
return (
|
||||
<SvgXmlIcon
|
||||
src = { src }
|
||||
{ ...props } />
|
||||
);
|
||||
}
|
||||
|
||||
return <DefaultIcon { ...props } />;
|
||||
};
|
||||
|
||||
export default withBranding;
|
||||
Reference in New Issue
Block a user