[MM-43331] No autofocus after RHS supression (#25593)

* No autofocus after RHS supression

* Add tests and some fixes

* Address feedback

* Fix test
Этот коммит содержится в:
Daniel Espino García
2023-12-11 11:32:44 +01:00
коммит произвёл GitHub
родитель 5b6b425cfc
Коммит 4890715b81
9 изменённых файлов: 181 добавлений и 46 удалений

Просмотреть файл

@@ -30,6 +30,7 @@ exports[`components/RhsThread should match snapshot 1`] = `
rootPostId="id"
/>
<Connect(ThreadViewer)
fromSuppressed={false}
isThreadView={false}
rootPostId="id"
useRelativeTimestamp={false}

Просмотреть файл

@@ -60,6 +60,7 @@ describe('components/RhsThread', () => {
actions,
directTeammate,
currentTeam,
fromSuppressed: false,
};
test('should match snapshot', () => {

Просмотреть файл

@@ -21,6 +21,7 @@ type Props = {
channel: Channel | null;
selected: Post | FakePost;
previousRhsState?: RhsState;
fromSuppressed: boolean;
}
const RhsThread = ({
@@ -29,6 +30,7 @@ const RhsThread = ({
posts,
selected,
previousRhsState,
fromSuppressed,
}: Props) => {
const dispatch = useDispatch();
@@ -59,6 +61,7 @@ const RhsThread = ({
rootPostId={selected.id}
useRelativeTimestamp={false}
isThreadView={false}
fromSuppressed={fromSuppressed}
/>
</div>
);

Просмотреть файл

@@ -20,6 +20,7 @@ import {
getSelectedPostId,
getSelectedPostCardId,
getPreviousRhsState,
getIsRhsSuppressed,
} from 'selectors/rhs';
import {RHSStates} from 'utils/constants';
@@ -41,6 +42,7 @@ function mapStateToProps(state: GlobalState, props: RouteComponentProps) {
return {
isExpanded: getIsRhsExpanded(state),
isOpen: getIsRhsOpen(state),
isSuppressed: getIsRhsSuppressed(state),
channel,
postRightVisible: Boolean(selectedPostId) && rhsState !== RHSStates.EDIT_HISTORY,
postCardVisible: Boolean(selectedPostCardId),

Просмотреть файл

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import RhsThread from 'components/rhs_thread';
import {TestHelper} from 'utils/test_helper';
import SidebarRight from './sidebar_right';
type Props = ComponentProps<typeof SidebarRight>;
function getBaseProps(): Props {
const channel = TestHelper.getChannelMock();
return {
actions: {
closeRightHandSide: jest.fn(),
openAtPrevious: jest.fn(),
openRHSSearch: jest.fn(),
setRhsExpanded: jest.fn(),
showChannelFiles: jest.fn(),
showChannelInfo: jest.fn(),
showPinnedPosts: jest.fn(),
updateSearchTerms: jest.fn(),
},
channel,
isChannelFiles: false,
isChannelInfo: false,
isChannelMembers: false,
isExpanded: false,
isOpen: false,
isPinnedPosts: false,
isPluginView: false,
isPostEditHistory: false,
isSuppressed: false,
postCardVisible: false,
postRightVisible: false,
previousRhsState: '',
productId: '',
rhsChannel: channel,
searchVisible: false,
selectedPostCardId: '',
selectedPostId: '',
team: TestHelper.getTeamMock(),
teamId: '',
};
}
describe('pass from suppressed', () => {
it('fromSuppressed is only passed when moving from suppressed state to non suppressed', () => {
const props = getBaseProps();
const wrapper = shallow(<SidebarRight {...props}/>);
expect(wrapper.find(RhsThread)).toHaveLength(0);
wrapper.setProps({isOpen: true, postRightVisible: true});
expect(wrapper.find(RhsThread)).toHaveLength(1);
expect(wrapper.find(RhsThread).props().fromSuppressed).toBeFalsy();
wrapper.setProps({isSuppressed: true, isOpen: false});
expect(wrapper.find(RhsThread)).toHaveLength(0);
wrapper.setProps({isSuppressed: false, isOpen: true});
expect(wrapper.find(RhsThread)).toHaveLength(1);
expect(wrapper.find(RhsThread).props().fromSuppressed).toBeTruthy();
wrapper.setProps({isOpen: false, postRightVisible: false});
expect(wrapper.find(RhsThread)).toHaveLength(0);
wrapper.setProps({isOpen: true, postRightVisible: true});
expect(wrapper.find(RhsThread)).toHaveLength(1);
expect(wrapper.find(RhsThread).props().fromSuppressed).toBeFalsy();
});
});

Просмотреть файл

@@ -28,6 +28,7 @@ import {isMac} from 'utils/user_agent';
import type {RhsState} from 'types/store/rhs';
export type Props = {
isSuppressed: boolean;
isExpanded: boolean;
isOpen: boolean;
channel: Channel;
@@ -68,6 +69,8 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
sidebarRightWidthHolder: React.RefObject<HTMLDivElement>;
previous: Partial<Props> | undefined = undefined;
focusSearchBar?: () => void;
lastOpenState = false;
lastSuppressedState = false;
constructor(props: Props) {
super(props);
@@ -149,6 +152,9 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
trackEvent('ui', 'ui_rhs_opened');
}
this.lastOpenState = this.props.isOpen;
this.lastSuppressedState = this.props.isSuppressed;
const {actions, isChannelFiles, isPinnedPosts, rhsChannel, channel} = this.props;
if (isPinnedPosts && prevProps.isPinnedPosts === isPinnedPosts && rhsChannel.id !== prevProps.rhsChannel.id) {
actions.showPinnedPosts(rhsChannel.id);
@@ -232,7 +238,10 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
content = (
<div className='post-right__container'>
<FileUploadOverlay overlayType='right'/>
<RhsThread previousRhsState={previousRhsState}/>
<RhsThread
previousRhsState={previousRhsState}
fromSuppressed={!this.lastOpenState && this.props.isOpen && this.lastSuppressedState}
/>
</div>
);
} else if (postCardVisible) {

Просмотреть файл

@@ -50,6 +50,7 @@ export type Props = Attrs & {
isThreadView?: boolean;
inputPlaceholder?: string;
rootPostId: string;
fromSuppressed?: boolean;
};
type State = {
@@ -224,6 +225,7 @@ export default class ThreadViewer extends React.PureComponent<Props, State> {
highlightedPostId={this.props.highlightedPostId}
selectedPostFocusedAt={this.props.selectedPostFocusedAt}
isThreadView={Boolean(this.props.isCollapsedThreadsEnabled && this.props.isThreadView)}
fromSuppressed={this.props.fromSuppressed}
/>
)}
</>

Просмотреть файл

@@ -1,71 +1,84 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import {shallow} from 'enzyme';
import type {ComponentProps} from 'react';
import React from 'react';
import type {Channel} from '@mattermost/types/channels';
import type {Post} from '@mattermost/types/posts';
import type {GlobalState} from '@mattermost/types/store';
import type {UserProfile} from '@mattermost/types/users';
import type {DeepPartial} from '@mattermost/types/utilities';
import {Permissions} from 'mattermost-redux/constants';
import {renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import VirtualizedThreadViewer from './virtualized_thread_viewer';
describe('components/threading/VirtualizedThreadViewer', () => {
const post: Post = TestHelper.getPostMock({
channel_id: 'channel_id',
create_at: 1502715365009,
update_at: 1502715372443,
is_following: true,
reply_count: 3,
});
// Needed for apply markdown to properly work down the line
global.ResizeObserver = require('resize-observer-polyfill');
const channel: Channel = TestHelper.getChannelMock({
display_name: '',
name: '',
header: '',
purpose: '',
creator_id: '',
scheme_id: '',
teammate_id: '',
status: '',
type Props = ComponentProps<typeof VirtualizedThreadViewer>;
function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
const channel = TestHelper.getChannelMock();
const currentUser = TestHelper.getUserMock({roles: 'role'});
const post = TestHelper.getPostMock({
channel_id: channel.id,
});
const actions = {
removePost: jest.fn(),
selectPostCard: jest.fn(),
getPostThread: jest.fn(),
getThread: jest.fn(),
updateThreadRead: jest.fn(),
updateThreadLastOpened: jest.fn(),
fetchRHSAppsBindings: jest.fn(),
};
const directTeammate: UserProfile = TestHelper.getUserMock();
const baseProps = {
const props: Props = {
selected: post,
channel,
currentUserId: 'user_id',
currentTeamId: 'team_id',
previewCollapsed: 'false',
previewEnabled: true,
socketConnectionStatus: true,
actions,
directTeammate,
posts: [post],
lastPost: post,
onCardClick: () => {},
onCardClickPost: () => {},
replyListIds: [],
teamId: '',
replyListIds: ['create-comment'],
useRelativeTimestamp: true,
isMobileView: false,
isThreadView: true,
isThreadView: false,
lastViewedAt: 0,
newMessagesSeparatorActions: [],
fromSuppressed: false,
};
const state: DeepPartial<GlobalState> = {
entities: {
users: {
currentUserId: currentUser.id,
profiles: {
[currentUser.id]: currentUser,
},
},
posts: {
posts: {
[post.id]: post,
},
},
channels: {
channels: {
[channel.id]: channel,
},
},
roles: {
roles: {
role: {
id: 'role',
name: 'role',
permissions: [Permissions.CREATE_POST, Permissions.USE_CHANNEL_MENTIONS],
},
},
},
},
};
return [props, state];
}
describe('components/threading/VirtualizedThreadViewer', () => {
const [baseProps] = getBasePropsAndState();
test('should scroll to the bottom when the current user makes a new post in the thread', () => {
const scrollToBottom = jest.fn();
@@ -80,7 +93,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
lastPost:
{
id: 'newpost',
root_id: post.id,
root_id: baseProps.selected.id,
user_id: 'user_id',
},
});
@@ -103,7 +116,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
lastPost:
{
id: 'newpost',
root_id: post.id,
root_id: baseProps.selected.id,
user_id: 'other_user_id',
},
});
@@ -127,7 +140,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
lastPost:
{
id: 'newpost',
root_id: post.id,
root_id: baseProps.selected.id,
user_id: 'user_id',
},
highlightedPostId: '42',
@@ -136,3 +149,32 @@ describe('components/threading/VirtualizedThreadViewer', () => {
expect(scrollToBottom).not.toHaveBeenCalled();
});
});
describe('fromSuppressed works as expected', () => {
// This setup is so AutoSizer renders its contents
const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight');
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth');
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {configurable: true, value: 50});
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {configurable: true, value: 50});
});
afterAll(() => {
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight!);
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth!);
});
it('autofocus if fromSuppressed is not set', () => {
const [props, state] = getBasePropsAndState();
renderWithContext(<VirtualizedThreadViewer {...props}/>, state);
expect(screen.getByRole('textbox')).toHaveFocus();
});
it('do not autofocus if fromSuppressed is set', () => {
const [props, state] = getBasePropsAndState();
props.fromSuppressed = true;
renderWithContext(<VirtualizedThreadViewer {...props}/>, state);
expect(screen.getByRole('textbox')).not.toHaveFocus();
});
});

Просмотреть файл

@@ -44,6 +44,7 @@ type Props = {
lastViewedAt: number;
newMessagesSeparatorActions: PluginComponent[];
inputPlaceholder?: string;
fromSuppressed?: boolean;
}
type State = {
@@ -357,7 +358,7 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
return (
<CreateComment
placeholder={this.props.inputPlaceholder}
focusOnMount={!this.props.isThreadView && (this.state.userScrolledToBottom || (!this.state.userScrolled && this.getInitialPostIndex() === 0))}
focusOnMount={!this.props.fromSuppressed && !this.props.isThreadView && (this.state.userScrolledToBottom || (!this.state.userScrolled && this.getInitialPostIndex() === 0))}
isThreadView={this.props.isThreadView}
latestPostId={this.props.lastPost.id}
ref={this.postCreateContainerRef}