[MM-43331] No autofocus after RHS supression (#25593)
* No autofocus after RHS supression * Add tests and some fixes * Address feedback * Fix test
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
5b6b425cfc
Коммит
4890715b81
@@ -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}
|
||||
|
||||
Ссылка в новой задаче
Block a user