init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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;
}
}

View 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();
}
}
}

View 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
}
};

View 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;

View File

@@ -0,0 +1,5 @@
export const COLORS = {
white: 'white',
green: 'green',
red: 'red'
};