mattermost-mobile-upstream/app/components/attribute_chip/index.tsx
Pablo Vélez 9183991a0e
MM-70813 - support multi value attribute chips (#10192)
* 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>
2026-09-30 12:08:22 +02:00

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;