diff --git a/webapp/channels/src/components/suggestion/switch_channel_provider.test.tsx b/webapp/channels/src/components/suggestion/switch_channel_provider.test.tsx index e89e93ff8c6..04e23faa2fc 100644 --- a/webapp/channels/src/components/suggestion/switch_channel_provider.test.tsx +++ b/webapp/channels/src/components/suggestion/switch_channel_provider.test.tsx @@ -52,6 +52,25 @@ jest.mock('mattermost-redux/actions/channels', () => ({ })), })); +jest.mock('components/with_tooltip', () => { + const ReactActual = jest.requireActual('react'); + const Mock = jest.fn(({children, title, disabled}: {children: React.ReactNode; title: unknown; disabled?: boolean}) => { + return ReactActual.createElement( + 'div', + { + 'data-testid': 'with-tooltip', + 'data-tooltip-title': typeof title === 'string' ? title : '', + 'data-tooltip-disabled': String(Boolean(disabled)), + }, + children, + ); + }); + return { + __esModule: true, + default: Mock, + }; +}); + describe('components/SwitchChannelProvider', () => { const defaultState = { entities: { @@ -1472,4 +1491,122 @@ describe('SwitchChannelSuggestion', () => { expect(suggestion).toHaveAccessibleName(channel3.display_name); expect(suggestion).toHaveAccessibleDescription(`5 unread notifications ~${channel3.name} Public channel`); }); + + describe('layout and tooltip behavior for long names', () => { + const longTeam1 = TestHelper.getTeamMock({ + id: 'team1', + display_name: 'A Very Long Team Display Name That Will Likely Overflow Its Slot In The Switcher', + }); + const longTeam2 = TestHelper.getTeamMock({ + id: 'team2', + display_name: 'Another Long Team Two', + }); + const longChannel = TestHelper.getChannelMock({ + id: 'channel1', + team_id: 'team1', + name: 'super_long_channel_name', + display_name: 'Super Extremely Long Channel Display Name That Should Truncate With An Ellipsis', + }); + + afterEach(() => { + // reset prototype overrides between tests + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {configurable: true, value: 0}); + Object.defineProperty(HTMLElement.prototype, 'clientWidth', {configurable: true, value: 0}); + }); + + test('should render team name as a sibling of the primary column wrapper inside .suggestion-list__flex when on multiple teams', () => { + renderWithContext( + , + getBaseState([longTeam1, longTeam2], [longChannel]), + ); + + const suggestion = document.getElementById(baseProps.id) as HTMLElement; + expect(suggestion).toBeInTheDocument(); + + // Both nodes (channel name and team name) are present + expect(screen.getByText(longChannel.display_name)).toBeInTheDocument(); + expect(screen.getByText(longTeam1.display_name)).toBeInTheDocument(); + + // The flex row contains the primary column wrapper and the team name as siblings + const flexRow = suggestion.querySelector('.suggestion-list__flex') as HTMLElement; + expect(flexRow).not.toBeNull(); + + const primaryColumn = flexRow.querySelector(':scope > .suggestion-list__switch-channel-primary'); + expect(primaryColumn).not.toBeNull(); + + const teamNameNode = flexRow.querySelector('.suggestion-list__team-name'); + expect(teamNameNode).not.toBeNull(); + expect(teamNameNode).toHaveTextContent(longTeam1.display_name); + + // Team name must live outside the primary column so it remains a flex sibling that doesn't shrink with the channel name. + expect(primaryColumn!.contains(teamNameNode)).toBe(false); + + // Channel name span should live inside the primary column with the truncation class + const channelNameNode = primaryColumn!.querySelector('.suggestion-list__channel-name-text'); + expect(channelNameNode).not.toBeNull(); + expect(channelNameNode).toHaveTextContent(longChannel.display_name); + }); + + test('should disable the channel-name tooltip when the channel name fits its container', () => { + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {configurable: true, value: 100}); + Object.defineProperty(HTMLElement.prototype, 'clientWidth', {configurable: true, value: 100}); + + renderWithContext( + , + getBaseState([longTeam1, longTeam2], [longChannel]), + ); + + const tooltips = screen.getAllByTestId('with-tooltip'); + const channelNameTooltip = tooltips.find((node) => node.getAttribute('data-tooltip-title') === longChannel.display_name); + expect(channelNameTooltip).toBeDefined(); + expect(channelNameTooltip).toHaveAttribute('data-tooltip-disabled', 'true'); + + const teamNameTooltip = tooltips.find((node) => node.getAttribute('data-tooltip-title') === longTeam1.display_name); + expect(teamNameTooltip).toBeDefined(); + expect(teamNameTooltip).toHaveAttribute('data-tooltip-disabled', 'true'); + }); + + test('should enable the channel-name tooltip when the channel name overflows its container', () => { + Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {configurable: true, value: 500}); + Object.defineProperty(HTMLElement.prototype, 'clientWidth', {configurable: true, value: 100}); + + renderWithContext( + , + getBaseState([longTeam1, longTeam2], [longChannel]), + ); + + const tooltips = screen.getAllByTestId('with-tooltip'); + const channelNameTooltip = tooltips.find((node) => node.getAttribute('data-tooltip-title') === longChannel.display_name); + expect(channelNameTooltip).toBeDefined(); + expect(channelNameTooltip).toHaveAttribute('data-tooltip-disabled', 'false'); + + const teamNameTooltip = tooltips.find((node) => node.getAttribute('data-tooltip-title') === longTeam1.display_name); + expect(teamNameTooltip).toBeDefined(); + expect(teamNameTooltip).toHaveAttribute('data-tooltip-disabled', 'false'); + }); + }); }); diff --git a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx index d3e7b48104a..1f75f8f6790 100644 --- a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx +++ b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import classNames from 'classnames'; -import React from 'react'; +import React, {useLayoutEffect, useRef, useState} from 'react'; import {defineMessage, useIntl} from 'react-intl'; import {connect, useSelector} from 'react-redux'; @@ -58,6 +58,7 @@ import ProfilePicture from 'components/profile_picture'; import SharedChannelIndicator from 'components/shared_channel_indicator'; import BotTag from 'components/widgets/tag/bot_tag'; import GuestTag from 'components/widgets/tag/guest_tag'; +import WithTooltip from 'components/with_tooltip'; import {getArchiveIconClassName} from 'utils/channel_utils'; import {Constants, StoragePrefixes} from 'utils/constants'; @@ -145,6 +146,11 @@ export const SwitchChannelSuggestion = React.forwardRef(({ const currentUserId = useSelector(getCurrentUserId); + const channelNameRef = useRef(null); + const [isChannelNameTruncated, setIsChannelNameTruncated] = useState(false); + const teamNameRef = useRef(null); + const [isTeamNameTruncated, setIsTeamNameTruncated] = useState(false); + const ids = usePrefixedIds(id, { name: null, channelType: null, @@ -320,18 +326,32 @@ export const SwitchChannelSuggestion = React.forwardRef(({ let teamName = null; if (isRealChannel(channel) && channel.team_id && team) { teamName = ( - - {team.display_name} - + + {team.display_name} + + ); } const showSlug = (isPartOfOnlyOneTeam || channel.type === Constants.DM_CHANNEL) && channel.type !== Constants.THREADS; Reflect.deleteProperty(otherProps, 'dispatch'); + useLayoutEffect(() => { + const channelEl = channelNameRef.current; + setIsChannelNameTruncated(Boolean(channelEl && channelEl.scrollWidth > channelEl.clientWidth)); + + const teamEl = teamNameRef.current; + setIsTeamNameTruncated(Boolean(teamEl && teamEl.scrollWidth > teamEl.clientWidth)); + }, [name, description, showSlug, isPartOfOnlyOneTeam, team?.display_name, item.unread, channelIsArchived]); + return ( (({ > {icon}
- - - {name} - - {showSlug && description && ( - + + - {description} - - )} - - {customStatus} - {sharedIcon} - {tag && {tag}} - {badge} + + {name} + + + {showSlug && description && ( + + {description} + + )} + + {customStatus} + {sharedIcon} + {tag && {tag}} + {badge} +
{!isPartOfOnlyOneTeam && teamName}
diff --git a/webapp/channels/src/sass/components/_suggestion-list.scss b/webapp/channels/src/sass/components/_suggestion-list.scss index 50c89854433..9f165e306c7 100644 --- a/webapp/channels/src/sass/components/_suggestion-list.scss +++ b/webapp/channels/src/sass/components/_suggestion-list.scss @@ -213,10 +213,6 @@ .modal & { padding: 8px 3.2rem; - - .suggestion-list__team-name { - right: 32px; - } } .suggestion-list__ellipsis { @@ -248,11 +244,25 @@ .suggestion-list__flex { display: flex; width: 100%; + min-width: 0; max-width: 100%; align-items: center; + .suggestion-list__switch-channel-primary { + display: flex; + overflow: hidden; + min-width: 0; + flex: 1 1 auto; + align-items: center; + } + .suggestion-list__main { + display: flex; + overflow: hidden; width: unset; + min-width: 0; + flex: 1 1 auto; + white-space: nowrap; > span:first-child { overflow: hidden; @@ -260,6 +270,28 @@ } } + .suggestion-list__channel-name-text { + overflow: hidden; + min-width: 0; + text-overflow: ellipsis; + white-space: nowrap; + } + + .suggestion-list_unread-mentions { + flex: 0 0 auto; + margin-left: auto; + } + + .suggestion-list__team-name { + position: static; + overflow: hidden; + max-width: 40%; + flex: 0 0 auto; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .badge { position: unset; display: flex; @@ -358,16 +390,6 @@ } } - .suggestion-list__team-name { - position: absolute; - right: 20px; - overflow: hidden; - max-width: 20%; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; - } - .Tag { flex-shrink: 0; }