mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-06 10:50:13 +00:00
* MM-70813 - support multi value attribute chips * add missing tests * adjust e2e test * update overflowSheet testID to match BottomSheetContent rendering * resolve attribute placeholders in the native channel banner fallback * channel info listing, overflow sheet, chip recolour, banner resubscriptions * show every header attribute and value in the +N overflow sheet * single banner pipeline, channel info memo and dedupe, utils placement, sheet row styles, e2e flag writes * honor channel banner toggle and drop classification color once its token is removed from the banner --------- Co-authored-by: Mattermost Build <build@mattermost.com>
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {useMemo} from 'react';
|
|
import {Text, View} from 'react-native';
|
|
|
|
import {NEUTRAL_CHIP_HEADER_BG, NEUTRAL_CHIP_HEADER_TEXT} from '@constants/channel_attributes';
|
|
import {useTheme} from '@context/theme';
|
|
import {getContrastingSimpleColor} from '@utils/general';
|
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|
import {typography} from '@utils/typography';
|
|
|
|
// Hard character cap per chip. Values longer than this are truncated with an
|
|
// ellipsis so one long label cannot consume the whole header row.
|
|
const MAX_CHARS = 15;
|
|
|
|
// Wraps several AttributeChips for one multi-valued attribute so they wrap
|
|
// onto additional lines instead of running off-screen, right-aligned to match
|
|
// a single chip's position. Shared by every surface that renders more than
|
|
// one chip per attribute (header overflow sheet, Channel Info), so the
|
|
// wrapping behavior can't drift between them.
|
|
export const attributeChipGroupStyle = {
|
|
flex: 1,
|
|
flexDirection: 'row' as const,
|
|
alignItems: 'center' as const,
|
|
flexWrap: 'wrap' as const,
|
|
justifyContent: 'flex-end' as const,
|
|
gap: 4,
|
|
};
|
|
|
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|
container: {
|
|
paddingHorizontal: 6,
|
|
borderRadius: 4,
|
|
justifyContent: 'center',
|
|
},
|
|
neutralContainer: {
|
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
|
|
},
|
|
|
|
// Neutral chip for the dark channel header: solid light-gray so it reads
|
|
// clearly on any sidebarBg colour. Text is dark to contrast the light background.
|
|
neutralContainerHeader: {
|
|
backgroundColor: NEUTRAL_CHIP_HEADER_BG,
|
|
},
|
|
text: {
|
|
...typography('Body', 25, 'SemiBold'),
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.2,
|
|
},
|
|
neutralText: {
|
|
color: changeOpacity(theme.centerChannelColor, 0.75),
|
|
},
|
|
neutralTextHeader: {
|
|
color: NEUTRAL_CHIP_HEADER_TEXT,
|
|
},
|
|
}));
|
|
|
|
type Props = {
|
|
|
|
// The attribute's name, used for the accessibility label so a screen reader
|
|
// hears which attribute the value belongs to.
|
|
label: string;
|
|
|
|
value: string;
|
|
|
|
// Hex from the option definition. Absent or malformed falls back to the
|
|
// neutral treatment rather than guessing at a readable foreground.
|
|
color?: string;
|
|
|
|
// False where the label is already visible beside the chip, so it is not
|
|
// announced twice.
|
|
announceLabel?: boolean;
|
|
|
|
// 'header' for chips inside the dark channel header; 'info' (default) for
|
|
// chips on a light surface such as Channel Info or the overflow sheet.
|
|
// Only affects the neutral fallback colours — option colours are unchanged.
|
|
variant?: 'header' | 'info';
|
|
|
|
testID?: string;
|
|
};
|
|
|
|
/**
|
|
* One channel attribute value, as a chip.
|
|
*
|
|
* The value is always rendered as text: colour must never be the only carrier of
|
|
* meaning. The background is administrator-chosen, so the foreground is derived
|
|
* from its luminance with getContrastingSimpleColor, the same helper the channel
|
|
* banner uses.
|
|
*
|
|
* A chip is information, not a control. It reports nothing about access, and no
|
|
* string here may suggest otherwise.
|
|
*/
|
|
const AttributeChip = ({label, value, color, announceLabel = true, variant = 'info', testID}: Props) => {
|
|
const theme = useTheme();
|
|
const styles = getStyleSheet(theme);
|
|
|
|
const {containerStyle, textStyle} = useMemo(() => {
|
|
if (color) {
|
|
const foreground = getContrastingSimpleColor(color);
|
|
if (foreground) {
|
|
return {
|
|
containerStyle: [styles.container, {backgroundColor: color}],
|
|
textStyle: [styles.text, {color: foreground}],
|
|
};
|
|
}
|
|
}
|
|
|
|
// Malformed hex or no color: use the neutral fallback for this variant.
|
|
const neutralBg = variant === 'header' ? styles.neutralContainerHeader : styles.neutralContainer;
|
|
const neutralFg = variant === 'header' ? styles.neutralTextHeader : styles.neutralText;
|
|
return {
|
|
containerStyle: [styles.container, neutralBg],
|
|
textStyle: [styles.text, neutralFg],
|
|
};
|
|
}, [color, variant, styles]);
|
|
|
|
const displayValue = value.length > MAX_CHARS ? `${value.slice(0, MAX_CHARS)}…` : value;
|
|
|
|
return (
|
|
<View
|
|
style={containerStyle}
|
|
testID={testID}
|
|
>
|
|
<Text
|
|
style={textStyle}
|
|
numberOfLines={1}
|
|
ellipsizeMode='tail'
|
|
accessibilityLabel={announceLabel ? `${label}: ${value}` : value}
|
|
testID={testID ? `${testID}.value` : undefined}
|
|
>
|
|
{displayValue}
|
|
</Text>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default AttributeChip;
|