[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"
|
rootPostId="id"
|
||||||
/>
|
/>
|
||||||
<Connect(ThreadViewer)
|
<Connect(ThreadViewer)
|
||||||
|
fromSuppressed={false}
|
||||||
isThreadView={false}
|
isThreadView={false}
|
||||||
rootPostId="id"
|
rootPostId="id"
|
||||||
useRelativeTimestamp={false}
|
useRelativeTimestamp={false}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ describe('components/RhsThread', () => {
|
|||||||
actions,
|
actions,
|
||||||
directTeammate,
|
directTeammate,
|
||||||
currentTeam,
|
currentTeam,
|
||||||
|
fromSuppressed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type Props = {
|
|||||||
channel: Channel | null;
|
channel: Channel | null;
|
||||||
selected: Post | FakePost;
|
selected: Post | FakePost;
|
||||||
previousRhsState?: RhsState;
|
previousRhsState?: RhsState;
|
||||||
|
fromSuppressed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RhsThread = ({
|
const RhsThread = ({
|
||||||
@@ -29,6 +30,7 @@ const RhsThread = ({
|
|||||||
posts,
|
posts,
|
||||||
selected,
|
selected,
|
||||||
previousRhsState,
|
previousRhsState,
|
||||||
|
fromSuppressed,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ const RhsThread = ({
|
|||||||
rootPostId={selected.id}
|
rootPostId={selected.id}
|
||||||
useRelativeTimestamp={false}
|
useRelativeTimestamp={false}
|
||||||
isThreadView={false}
|
isThreadView={false}
|
||||||
|
fromSuppressed={fromSuppressed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
getSelectedPostId,
|
getSelectedPostId,
|
||||||
getSelectedPostCardId,
|
getSelectedPostCardId,
|
||||||
getPreviousRhsState,
|
getPreviousRhsState,
|
||||||
|
getIsRhsSuppressed,
|
||||||
} from 'selectors/rhs';
|
} from 'selectors/rhs';
|
||||||
|
|
||||||
import {RHSStates} from 'utils/constants';
|
import {RHSStates} from 'utils/constants';
|
||||||
@@ -41,6 +42,7 @@ function mapStateToProps(state: GlobalState, props: RouteComponentProps) {
|
|||||||
return {
|
return {
|
||||||
isExpanded: getIsRhsExpanded(state),
|
isExpanded: getIsRhsExpanded(state),
|
||||||
isOpen: getIsRhsOpen(state),
|
isOpen: getIsRhsOpen(state),
|
||||||
|
isSuppressed: getIsRhsSuppressed(state),
|
||||||
channel,
|
channel,
|
||||||
postRightVisible: Boolean(selectedPostId) && rhsState !== RHSStates.EDIT_HISTORY,
|
postRightVisible: Boolean(selectedPostId) && rhsState !== RHSStates.EDIT_HISTORY,
|
||||||
postCardVisible: Boolean(selectedPostCardId),
|
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';
|
import type {RhsState} from 'types/store/rhs';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
isSuppressed: boolean;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
@@ -68,6 +69,8 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
sidebarRightWidthHolder: React.RefObject<HTMLDivElement>;
|
sidebarRightWidthHolder: React.RefObject<HTMLDivElement>;
|
||||||
previous: Partial<Props> | undefined = undefined;
|
previous: Partial<Props> | undefined = undefined;
|
||||||
focusSearchBar?: () => void;
|
focusSearchBar?: () => void;
|
||||||
|
lastOpenState = false;
|
||||||
|
lastSuppressedState = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -149,6 +152,9 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
trackEvent('ui', 'ui_rhs_opened');
|
trackEvent('ui', 'ui_rhs_opened');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastOpenState = this.props.isOpen;
|
||||||
|
this.lastSuppressedState = this.props.isSuppressed;
|
||||||
|
|
||||||
const {actions, isChannelFiles, isPinnedPosts, rhsChannel, channel} = this.props;
|
const {actions, isChannelFiles, isPinnedPosts, rhsChannel, channel} = this.props;
|
||||||
if (isPinnedPosts && prevProps.isPinnedPosts === isPinnedPosts && rhsChannel.id !== prevProps.rhsChannel.id) {
|
if (isPinnedPosts && prevProps.isPinnedPosts === isPinnedPosts && rhsChannel.id !== prevProps.rhsChannel.id) {
|
||||||
actions.showPinnedPosts(rhsChannel.id);
|
actions.showPinnedPosts(rhsChannel.id);
|
||||||
@@ -232,7 +238,10 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
content = (
|
content = (
|
||||||
<div className='post-right__container'>
|
<div className='post-right__container'>
|
||||||
<FileUploadOverlay overlayType='right'/>
|
<FileUploadOverlay overlayType='right'/>
|
||||||
<RhsThread previousRhsState={previousRhsState}/>
|
<RhsThread
|
||||||
|
previousRhsState={previousRhsState}
|
||||||
|
fromSuppressed={!this.lastOpenState && this.props.isOpen && this.lastSuppressedState}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (postCardVisible) {
|
} else if (postCardVisible) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type Props = Attrs & {
|
|||||||
isThreadView?: boolean;
|
isThreadView?: boolean;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
rootPostId: string;
|
rootPostId: string;
|
||||||
|
fromSuppressed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -224,6 +225,7 @@ export default class ThreadViewer extends React.PureComponent<Props, State> {
|
|||||||
highlightedPostId={this.props.highlightedPostId}
|
highlightedPostId={this.props.highlightedPostId}
|
||||||
selectedPostFocusedAt={this.props.selectedPostFocusedAt}
|
selectedPostFocusedAt={this.props.selectedPostFocusedAt}
|
||||||
isThreadView={Boolean(this.props.isCollapsedThreadsEnabled && this.props.isThreadView)}
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {screen} from '@testing-library/react';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
import type {ComponentProps} from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type {Channel} from '@mattermost/types/channels';
|
import type {GlobalState} from '@mattermost/types/store';
|
||||||
import type {Post} from '@mattermost/types/posts';
|
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
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 {TestHelper} from 'utils/test_helper';
|
||||||
|
|
||||||
import VirtualizedThreadViewer from './virtualized_thread_viewer';
|
import VirtualizedThreadViewer from './virtualized_thread_viewer';
|
||||||
|
|
||||||
describe('components/threading/VirtualizedThreadViewer', () => {
|
// Needed for apply markdown to properly work down the line
|
||||||
const post: Post = TestHelper.getPostMock({
|
global.ResizeObserver = require('resize-observer-polyfill');
|
||||||
channel_id: 'channel_id',
|
|
||||||
create_at: 1502715365009,
|
|
||||||
update_at: 1502715372443,
|
|
||||||
is_following: true,
|
|
||||||
reply_count: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const channel: Channel = TestHelper.getChannelMock({
|
type Props = ComponentProps<typeof VirtualizedThreadViewer>;
|
||||||
display_name: '',
|
function getBasePropsAndState(): [Props, DeepPartial<GlobalState>] {
|
||||||
name: '',
|
const channel = TestHelper.getChannelMock();
|
||||||
header: '',
|
const currentUser = TestHelper.getUserMock({roles: 'role'});
|
||||||
purpose: '',
|
const post = TestHelper.getPostMock({
|
||||||
creator_id: '',
|
channel_id: channel.id,
|
||||||
scheme_id: '',
|
|
||||||
teammate_id: '',
|
|
||||||
status: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 directTeammate: UserProfile = TestHelper.getUserMock();
|
||||||
|
const props: Props = {
|
||||||
const baseProps = {
|
|
||||||
selected: post,
|
selected: post,
|
||||||
channel,
|
channel,
|
||||||
currentUserId: 'user_id',
|
currentUserId: 'user_id',
|
||||||
currentTeamId: 'team_id',
|
|
||||||
previewCollapsed: 'false',
|
|
||||||
previewEnabled: true,
|
|
||||||
socketConnectionStatus: true,
|
|
||||||
actions,
|
|
||||||
directTeammate,
|
directTeammate,
|
||||||
posts: [post],
|
|
||||||
lastPost: post,
|
lastPost: post,
|
||||||
onCardClick: () => {},
|
onCardClick: () => {},
|
||||||
onCardClickPost: () => {},
|
replyListIds: ['create-comment'],
|
||||||
replyListIds: [],
|
|
||||||
teamId: '',
|
|
||||||
useRelativeTimestamp: true,
|
useRelativeTimestamp: true,
|
||||||
isMobileView: false,
|
isMobileView: false,
|
||||||
isThreadView: true,
|
isThreadView: false,
|
||||||
lastViewedAt: 0,
|
lastViewedAt: 0,
|
||||||
newMessagesSeparatorActions: [],
|
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', () => {
|
test('should scroll to the bottom when the current user makes a new post in the thread', () => {
|
||||||
const scrollToBottom = jest.fn();
|
const scrollToBottom = jest.fn();
|
||||||
|
|
||||||
@@ -80,7 +93,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
|
|||||||
lastPost:
|
lastPost:
|
||||||
{
|
{
|
||||||
id: 'newpost',
|
id: 'newpost',
|
||||||
root_id: post.id,
|
root_id: baseProps.selected.id,
|
||||||
user_id: 'user_id',
|
user_id: 'user_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -103,7 +116,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
|
|||||||
lastPost:
|
lastPost:
|
||||||
{
|
{
|
||||||
id: 'newpost',
|
id: 'newpost',
|
||||||
root_id: post.id,
|
root_id: baseProps.selected.id,
|
||||||
user_id: 'other_user_id',
|
user_id: 'other_user_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -127,7 +140,7 @@ describe('components/threading/VirtualizedThreadViewer', () => {
|
|||||||
lastPost:
|
lastPost:
|
||||||
{
|
{
|
||||||
id: 'newpost',
|
id: 'newpost',
|
||||||
root_id: post.id,
|
root_id: baseProps.selected.id,
|
||||||
user_id: 'user_id',
|
user_id: 'user_id',
|
||||||
},
|
},
|
||||||
highlightedPostId: '42',
|
highlightedPostId: '42',
|
||||||
@@ -136,3 +149,32 @@ describe('components/threading/VirtualizedThreadViewer', () => {
|
|||||||
expect(scrollToBottom).not.toHaveBeenCalled();
|
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;
|
lastViewedAt: number;
|
||||||
newMessagesSeparatorActions: PluginComponent[];
|
newMessagesSeparatorActions: PluginComponent[];
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
|
fromSuppressed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -357,7 +358,7 @@ class ThreadViewerVirtualized extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<CreateComment
|
<CreateComment
|
||||||
placeholder={this.props.inputPlaceholder}
|
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}
|
isThreadView={this.props.isThreadView}
|
||||||
latestPostId={this.props.lastPost.id}
|
latestPostId={this.props.lastPost.id}
|
||||||
ref={this.postCreateContainerRef}
|
ref={this.postCreateContainerRef}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user