This commit is contained in:
102
react/features/base/label/components/native/ExpandedLabel.tsx
Normal file
102
react/features/base/label/components/native/ExpandedLabel.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Animated, Text, View } from 'react-native';
|
||||
|
||||
import styles, { DEFAULT_COLOR } from './styles';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* The position of the parent element (from right to left) to display the
|
||||
* arrow.
|
||||
*/
|
||||
parentPosition: number;
|
||||
|
||||
/**
|
||||
* Custom styles.
|
||||
*/
|
||||
style?: Object;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The opacity animation Object.
|
||||
*/
|
||||
opacityAnimation: Animated.Value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A react {@code Component} that implements an expanded label as tooltip-like
|
||||
* component to explain the meaning of the {@code Label}.
|
||||
*/
|
||||
export default abstract class ExpandedLabel<P extends IProps> extends Component<P, IState> {
|
||||
/**
|
||||
* Instantiates a new {@code ExpandedLabel} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
opacityAnimation: new Animated.Value(0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
Animated.decay(this.state.opacityAnimation, {
|
||||
toValue: 1,
|
||||
velocity: 1,
|
||||
useNativeDriver: true
|
||||
} as Animated.DecayAnimationConfig).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Animated.View
|
||||
style = { [ styles.expandedLabelContainer,
|
||||
this.props.style,
|
||||
{ opacity: this.state.opacityAnimation }
|
||||
] }>
|
||||
<View
|
||||
style = { [ styles.expandedLabelTextContainer,
|
||||
{ backgroundColor: this._getColor() || DEFAULT_COLOR } ] }>
|
||||
<Text style = { styles.expandedLabelText }>
|
||||
{ this._getLabel() }
|
||||
</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label that needs to be rendered in the box. To be implemented
|
||||
* by its overriding classes.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
abstract _getLabel(): string;
|
||||
|
||||
/**
|
||||
* Defines the color of the expanded label. This function returns a default
|
||||
* value if implementing classes don't override it, but the goal is to have
|
||||
* expanded labels matching to circular labels in color.
|
||||
* If implementing classes return a falsy value, it also uses the default
|
||||
* color.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getColor() {
|
||||
return DEFAULT_COLOR;
|
||||
}
|
||||
}
|
||||
179
react/features/base/label/components/native/Label.tsx
Normal file
179
react/features/base/label/components/native/Label.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Animated, Text, ViewStyle } from 'react-native';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { StyleType, combineStyles } from '../../../styles/functions.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Const for status string 'in progress'.
|
||||
*/
|
||||
const STATUS_IN_PROGRESS = 'in_progress';
|
||||
|
||||
/**
|
||||
* Const for status string 'off'.
|
||||
*/
|
||||
const STATUS_OFF = 'off';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* An SVG icon to be rendered as the content of the label.
|
||||
*/
|
||||
icon?: Function;
|
||||
|
||||
/**
|
||||
* Color for the icon.
|
||||
*/
|
||||
iconColor?: string;
|
||||
|
||||
/**
|
||||
* Status of the label. This prop adds some additional styles based on its
|
||||
* value. E.g. If status = off, it will render the label symbolising that
|
||||
* the thing it displays (e.g. Recording) is off.
|
||||
*/
|
||||
status?: 'in_progress' | 'off' | 'on';
|
||||
|
||||
/**
|
||||
* Style of the label.
|
||||
*/
|
||||
style?: StyleType;
|
||||
|
||||
/**
|
||||
* String or component that will be rendered as the label itself.
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* Custom styles for the text.
|
||||
*/
|
||||
textStyle?: StyleType;
|
||||
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* An animation object handling the opacity changes of the in progress
|
||||
* label.
|
||||
*/
|
||||
pulseAnimation: Animated.Value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a circular indicator to be used for status icons, such as recording
|
||||
* on, audio-only conference, video quality and similar.
|
||||
*/
|
||||
export default class Label extends Component<IProps, State> {
|
||||
/**
|
||||
* A reference to the started animation of this label.
|
||||
*/
|
||||
animationReference: Animated.CompositeAnimation;
|
||||
|
||||
/**
|
||||
* Instantiates a new instance of {@code Label}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pulseAnimation: new Animated.Value(0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this._maybeToggleAnimation({}, this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: IProps) {
|
||||
this._maybeToggleAnimation(prevProps, this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React {@link Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { icon, text, status, style, iconColor, textStyle } = this.props;
|
||||
|
||||
let extraStyle = null;
|
||||
|
||||
switch (status) {
|
||||
case STATUS_IN_PROGRESS:
|
||||
extraStyle = {
|
||||
opacity: this.state.pulseAnimation
|
||||
};
|
||||
break;
|
||||
case STATUS_OFF:
|
||||
extraStyle = styles.labelOff;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style = { [
|
||||
combineStyles(styles.labelContainer, style ?? {}),
|
||||
extraStyle as ViewStyle
|
||||
] }>
|
||||
{ icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '18'
|
||||
src = { icon } /> }
|
||||
{ text && <Text style = { [ styles.labelText, textStyle ] }>
|
||||
{ text }
|
||||
</Text>}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the animation has to be started or stopped and acts
|
||||
* accordingly.
|
||||
*
|
||||
* @param {IProps} oldProps - The previous values of the Props.
|
||||
* @param {IProps} newProps - The new values of the Props.
|
||||
* @returns {void}
|
||||
*/
|
||||
_maybeToggleAnimation(oldProps: Partial<IProps>, newProps: IProps) {
|
||||
const { status: oldStatus } = oldProps;
|
||||
const { status: newStatus } = newProps;
|
||||
const { pulseAnimation } = this.state;
|
||||
|
||||
if (newStatus === STATUS_IN_PROGRESS
|
||||
&& oldStatus !== STATUS_IN_PROGRESS) {
|
||||
// Animation must be started
|
||||
this.animationReference = Animated.loop(Animated.sequence([
|
||||
Animated.timing(pulseAnimation, {
|
||||
delay: 500,
|
||||
toValue: 1,
|
||||
useNativeDriver: true
|
||||
}),
|
||||
Animated.timing(pulseAnimation, {
|
||||
toValue: 0.3,
|
||||
useNativeDriver: true
|
||||
})
|
||||
]));
|
||||
|
||||
this.animationReference.start();
|
||||
} else if (this.animationReference
|
||||
&& newStatus !== STATUS_IN_PROGRESS
|
||||
&& oldStatus === STATUS_IN_PROGRESS) {
|
||||
// Animation must be stopped
|
||||
this.animationReference.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
68
react/features/base/label/components/native/styles.ts
Normal file
68
react/features/base/label/components/native/styles.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ColorPalette } from '../../../styles/components/styles/ColorPalette';
|
||||
import BaseTheme from '../../../ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* The default color of the {@code Label} and {@code ExpandedLabel}.
|
||||
*/
|
||||
export const DEFAULT_COLOR = '#36383C';
|
||||
|
||||
/**
|
||||
* Margin of the {@Label} - to be reused when rendering the
|
||||
* {@code ExpandedLabel}.
|
||||
*/
|
||||
export const LABEL_MARGIN = 8;
|
||||
|
||||
/**
|
||||
* Size of the {@Label} - to be reused when rendering the
|
||||
* {@code ExpandedLabel}.
|
||||
*/
|
||||
export const LABEL_SIZE = 28;
|
||||
|
||||
/**
|
||||
* The styles of the native base/label feature.
|
||||
*/
|
||||
export default {
|
||||
expandedLabelContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 36,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
expandedLabelTextContainer: {
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: LABEL_MARGIN,
|
||||
paddingVertical: LABEL_MARGIN / 2
|
||||
},
|
||||
|
||||
expandedLabelText: {
|
||||
color: ColorPalette.white
|
||||
},
|
||||
|
||||
/**
|
||||
* The outermost view.
|
||||
*/
|
||||
labelContainer: {
|
||||
alignItems: 'space-between',
|
||||
backgroundColor: DEFAULT_COLOR,
|
||||
borderRadius: 3,
|
||||
flex: 0,
|
||||
height: LABEL_SIZE,
|
||||
justifyContent: 'center',
|
||||
marginLeft: LABEL_MARGIN,
|
||||
marginBottom: LABEL_MARGIN,
|
||||
paddingHorizontal: 8
|
||||
},
|
||||
|
||||
labelText: {
|
||||
color: ColorPalette.white,
|
||||
...BaseTheme.typography.labelBold
|
||||
},
|
||||
|
||||
labelOff: {
|
||||
opacity: 0.3
|
||||
}
|
||||
};
|
||||
134
react/features/base/label/components/web/Label.tsx
Normal file
134
react/features/base/label/components/web/Label.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { COLORS } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Optional label for screen reader users, invisible in the UI.
|
||||
*
|
||||
* Note: if the text prop is set, a screen reader will first announce
|
||||
* the accessibilityText, then the text.
|
||||
*/
|
||||
accessibilityText?: string;
|
||||
|
||||
/**
|
||||
* Own CSS class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The color of the label.
|
||||
*/
|
||||
color?: string;
|
||||
|
||||
/**
|
||||
* An SVG icon to be rendered as the content of the label.
|
||||
*/
|
||||
icon?: Function;
|
||||
|
||||
/**
|
||||
* Color for the icon.
|
||||
*/
|
||||
iconColor?: string;
|
||||
|
||||
/**
|
||||
* HTML ID attribute to add to the root of {@code Label}.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Click handler if any.
|
||||
*/
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* String or component that will be rendered as the label itself.
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
label: {
|
||||
...theme.typography.labelRegular,
|
||||
alignItems: 'center',
|
||||
background: theme.palette.ui04,
|
||||
borderRadius: '4px',
|
||||
color: theme.palette.text01,
|
||||
display: 'flex',
|
||||
margin: '0 2px',
|
||||
padding: '6px',
|
||||
height: 28,
|
||||
boxSizing: 'border-box'
|
||||
},
|
||||
withIcon: {
|
||||
marginLeft: 8
|
||||
},
|
||||
clickable: {
|
||||
cursor: 'pointer'
|
||||
},
|
||||
[COLORS.white]: {
|
||||
background: theme.palette.ui09,
|
||||
color: theme.palette.text04,
|
||||
|
||||
'& svg': {
|
||||
fill: theme.palette.icon04
|
||||
}
|
||||
},
|
||||
[COLORS.green]: {
|
||||
background: theme.palette.success02
|
||||
},
|
||||
[COLORS.red]: {
|
||||
background: theme.palette.actionDanger
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Label = ({
|
||||
accessibilityText,
|
||||
className,
|
||||
color,
|
||||
icon,
|
||||
iconColor,
|
||||
id,
|
||||
onClick,
|
||||
text
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const onKeyPress = useCallback(event => {
|
||||
if (!onClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}, [ onClick ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.label, onClick && classes.clickable,
|
||||
color && classes[color], className
|
||||
) }
|
||||
id = { id }
|
||||
onClick = { onClick }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = { onClick ? 'button' : undefined }
|
||||
tabIndex = { onClick ? 0 : undefined }>
|
||||
{icon && <Icon
|
||||
color = { iconColor }
|
||||
size = '16'
|
||||
src = { icon } />}
|
||||
{accessibilityText && <span className = 'sr-only'>{accessibilityText}</span>}
|
||||
{text && <span className = { icon && classes.withIcon }>{text}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Label;
|
||||
5
react/features/base/label/constants.ts
Normal file
5
react/features/base/label/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const COLORS = {
|
||||
white: 'white',
|
||||
green: 'green',
|
||||
red: 'red'
|
||||
};
|
||||
Reference in New Issue
Block a user