Add onPress on full chip (#9134)
* Add onPress on full chip * Address feedback * Fix test
This commit is contained in:
parent
3c6ae12634
commit
d4fb05d4bb
12 changed files with 191 additions and 57 deletions
|
|
@ -11,6 +11,7 @@ import BaseChip from './base_chip';
|
|||
|
||||
describe('BaseChip', () => {
|
||||
const onPressMock = jest.fn();
|
||||
const onActionPressMock = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -28,66 +29,66 @@ describe('BaseChip', () => {
|
|||
expect(getByText('Test Label')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with the X button when actionIcon is remove', () => {
|
||||
it('should render with the X button when action is remove', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon='remove'
|
||||
action={{icon: 'remove'}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('base_chip.remove.button')).toBeTruthy();
|
||||
expect(getByTestId('base_chip.remove.icon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with the chevron down button when actionIcon is downArrow', () => {
|
||||
it('should render with the chevron down button when action is downArrow', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon='downArrow'
|
||||
action={{icon: 'downArrow'}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getByTestId('base_chip.downArrow.button')).toBeTruthy();
|
||||
expect(getByTestId('base_chip.downArrow.icon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render the X button when actionIcon is undefined', () => {
|
||||
it('should not render the action button when action is undefined', () => {
|
||||
const {queryByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByTestId('base_chip.remove.button')).toBeNull();
|
||||
expect(queryByTestId('base_chip.remove.icon')).toBeNull();
|
||||
expect(queryByTestId('base_chip.downArrow.icon')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call onPress when the X button is pressed', () => {
|
||||
it('should call onPressMock when the action button is pressed', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon='remove'
|
||||
action={{icon: 'remove', onPress: onActionPressMock}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('base_chip.remove.button'));
|
||||
expect(onPressMock).toHaveBeenCalledTimes(1);
|
||||
expect(onPressMock).not.toHaveBeenCalled();
|
||||
expect(onActionPressMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onPress when the chip is pressed without X button', () => {
|
||||
it('should call onPress when the chip is pressed without action button', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
actionIcon={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
@ -95,6 +96,21 @@ describe('BaseChip', () => {
|
|||
expect(onPressMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onPress when the chip is pressed and we have an action button', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
onPress={onPressMock}
|
||||
label='Test Label'
|
||||
testID='base_chip'
|
||||
action={{icon: 'remove', onPress: onActionPressMock}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.press(getByTestId('base_chip.chip_button'));
|
||||
expect(onPressMock).toHaveBeenCalledTimes(1);
|
||||
expect(onActionPressMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle animations when showAnimation is true', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<BaseChip
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {Platform, Text, TouchableOpacity} from 'react-native';
|
||||
import {Platform, Text, TouchableOpacity, View} from 'react-native';
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -17,7 +17,10 @@ import {CHIP_HEIGHT} from './constants';
|
|||
type SelectedChipProps = {
|
||||
onPress?: () => void;
|
||||
testID?: string;
|
||||
actionIcon?: 'remove' | 'downArrow';
|
||||
action?: {
|
||||
icon: 'remove' | 'downArrow';
|
||||
onPress?: () => void;
|
||||
};
|
||||
showAnimation?: boolean;
|
||||
label: string;
|
||||
prefix?: JSX.Element;
|
||||
|
|
@ -70,7 +73,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
export default function BaseChip({
|
||||
testID,
|
||||
onPress,
|
||||
actionIcon,
|
||||
action,
|
||||
showAnimation,
|
||||
label,
|
||||
prefix,
|
||||
|
|
@ -78,6 +81,9 @@ export default function BaseChip({
|
|||
type = 'normal',
|
||||
boldText = false,
|
||||
}: SelectedChipProps) {
|
||||
const actionIcon = action?.icon;
|
||||
const actionOnPress = action?.onPress;
|
||||
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
|
@ -118,30 +124,49 @@ export default function BaseChip({
|
|||
let content = chipContent;
|
||||
if (actionIcon) {
|
||||
const iconName = actionIcon === 'remove' ? 'close-circle' : 'chevron-down';
|
||||
content = (
|
||||
<>
|
||||
{chipContent}
|
||||
let icon = (
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={16}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
testID={`${testID}.${actionIcon}.icon`}
|
||||
/>
|
||||
);
|
||||
|
||||
if (actionOnPress) {
|
||||
icon = (
|
||||
<TouchableOpacity
|
||||
style={style.remove}
|
||||
onPress={onPress}
|
||||
onPress={actionOnPress}
|
||||
testID={`${testID}.${actionIcon}.button`}
|
||||
>
|
||||
<CompassIcon
|
||||
name={iconName}
|
||||
size={16}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
{icon}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else {
|
||||
icon = (
|
||||
<View style={style.remove}>
|
||||
{icon}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<>
|
||||
{content}
|
||||
{icon}
|
||||
</>
|
||||
);
|
||||
} else if (onPress) {
|
||||
}
|
||||
|
||||
if (onPress) {
|
||||
content = (
|
||||
<TouchableOpacity
|
||||
style={style.chipContent}
|
||||
onPress={onPress}
|
||||
testID={`${testID}.chip_button`}
|
||||
>
|
||||
{chipContent}
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ describe('SelectedChip', () => {
|
|||
|
||||
const baseChip = getByTestId('selected-chip');
|
||||
expect(baseChip.props.label).toBe('Test Chip');
|
||||
expect(baseChip.props.actionIcon).toBe('remove');
|
||||
expect(baseChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
|
||||
expect(baseChip.props.showAnimation).toBe(true);
|
||||
expect(baseChip.props.prefix).toBeUndefined();
|
||||
baseChip.props.onPress();
|
||||
baseChip.props.action.onPress();
|
||||
expect(mockOnRemove).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnRemove).toHaveBeenCalledWith('test-id');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import BaseChip from './base_chip';
|
||||
|
||||
|
|
@ -18,15 +18,12 @@ export default function SelectedChip({
|
|||
onRemove,
|
||||
testID,
|
||||
}: SelectedChipProps) {
|
||||
const onPress = useCallback(() => {
|
||||
onRemove(id);
|
||||
}, [onRemove, id]);
|
||||
const action = useMemo(() => ({icon: 'remove' as const, onPress: () => onRemove(id)}), [id, onRemove]);
|
||||
|
||||
return (
|
||||
<BaseChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
actionIcon='remove'
|
||||
action={action}
|
||||
showAnimation={true}
|
||||
label={text}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ describe('SelectedUserChip', () => {
|
|||
const userChip = getByTestId('selected-user-chip');
|
||||
expect(userChip.props.user).toBe(mockUser);
|
||||
expect(userChip.props.teammateNameDisplay).toBe('username');
|
||||
expect(userChip.props.actionIcon).toBe('remove');
|
||||
expect(userChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
|
||||
expect(userChip.props.showAnimation).toBe(true);
|
||||
userChip.props.onPress();
|
||||
userChip.props.action.onPress();
|
||||
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import UserChip from './user_chip';
|
||||
|
||||
|
|
@ -20,11 +20,11 @@ export default function SelectedUserChip({
|
|||
teammateNameDisplay,
|
||||
onPress,
|
||||
}: SelectedChipProps) {
|
||||
const action = useMemo(() => ({icon: 'remove' as const, onPress}), [onPress]);
|
||||
return (
|
||||
<UserChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
actionIcon='remove'
|
||||
action={action}
|
||||
showAnimation={true}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
user={user}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ jest.mocked(ProfilePicture).mockImplementation((props) => React.createElement('P
|
|||
|
||||
describe('UserChip', () => {
|
||||
const mockOnPress = jest.fn();
|
||||
const mockOnActionPress = jest.fn();
|
||||
const mockUser = TestHelper.fakeUser({id: 'user-id', username: 'test-user'});
|
||||
|
||||
it('should render with the correct props', () => {
|
||||
|
|
@ -34,13 +35,13 @@ describe('UserChip', () => {
|
|||
testID='user-chip'
|
||||
teammateNameDisplay='username'
|
||||
showAnimation={true}
|
||||
actionIcon='remove'
|
||||
action={{icon: 'remove', onPress: mockOnActionPress}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const baseChip = getByTestId('user-chip');
|
||||
expect(baseChip.props.label).toBe('test-user');
|
||||
expect(baseChip.props.actionIcon).toBe('remove');
|
||||
expect(baseChip.props.action).toEqual({icon: 'remove', onPress: expect.any(Function)});
|
||||
expect(baseChip.props.showAnimation).toBe(true);
|
||||
|
||||
expect(baseChip.props.prefix).toBeDefined();
|
||||
|
|
@ -53,5 +54,42 @@ describe('UserChip', () => {
|
|||
baseChip.props.onPress();
|
||||
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnPress).toHaveBeenCalledWith('user-id');
|
||||
expect(mockOnActionPress).not.toHaveBeenCalled();
|
||||
|
||||
mockOnPress.mockClear();
|
||||
|
||||
baseChip.props.action.onPress();
|
||||
expect(mockOnActionPress).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnActionPress).toHaveBeenCalledWith('user-id');
|
||||
expect(mockOnPress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should leave action onPress undefined when action with no onPress is provided', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<UserChip
|
||||
user={mockUser}
|
||||
onPress={mockOnPress}
|
||||
teammateNameDisplay='username'
|
||||
action={{icon: 'remove'}}
|
||||
testID='user-chip'
|
||||
/>,
|
||||
);
|
||||
|
||||
const baseChip = getByTestId('user-chip');
|
||||
expect(baseChip.props.action.onPress).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should leave onPress undefined when onPress is not provided', () => {
|
||||
const {getByTestId} = renderWithIntlAndTheme(
|
||||
<UserChip
|
||||
user={mockUser}
|
||||
teammateNameDisplay='username'
|
||||
action={{icon: 'remove', onPress: mockOnActionPress}}
|
||||
testID='user-chip'
|
||||
/>,
|
||||
);
|
||||
|
||||
const baseChip = getByTestId('user-chip');
|
||||
expect(baseChip.props.onPress).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import React, {useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
|
|
@ -13,10 +13,13 @@ import type UserModel from '@typings/database/models/servers/user';
|
|||
|
||||
type SelectedChipProps = {
|
||||
user: UserModel | UserProfile;
|
||||
onPress: (id: string) => void;
|
||||
onPress?: (id: string) => void;
|
||||
testID?: string;
|
||||
teammateNameDisplay: string;
|
||||
actionIcon?: 'remove' | 'downArrow';
|
||||
action?: {
|
||||
icon: 'remove' | 'downArrow';
|
||||
onPress?: (id: string) => void;
|
||||
};
|
||||
showAnimation?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -25,15 +28,26 @@ export default function UserChip({
|
|||
user,
|
||||
teammateNameDisplay,
|
||||
onPress: receivedOnPress,
|
||||
actionIcon,
|
||||
action: receivedAction,
|
||||
showAnimation,
|
||||
}: SelectedChipProps) {
|
||||
const intl = useIntl();
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
receivedOnPress(user.id);
|
||||
const onPress = useMemo(() => {
|
||||
if (!receivedOnPress) {
|
||||
return undefined;
|
||||
}
|
||||
return () => receivedOnPress(user.id);
|
||||
}, [receivedOnPress, user.id]);
|
||||
|
||||
const action = useMemo(() => {
|
||||
if (!receivedAction) {
|
||||
return undefined;
|
||||
}
|
||||
const onActionPress = receivedAction.onPress ? (() => receivedAction.onPress?.(user.id)) : undefined;
|
||||
return {icon: receivedAction.icon, onPress: onActionPress};
|
||||
}, [receivedAction, user.id]);
|
||||
|
||||
const name = displayUsername(user, intl.locale, teammateNameDisplay);
|
||||
const picture = useMemo(() => (
|
||||
<ProfilePicture
|
||||
|
|
@ -49,7 +63,7 @@ export default function UserChip({
|
|||
<BaseChip
|
||||
testID={testID}
|
||||
onPress={onPress}
|
||||
actionIcon={actionIcon}
|
||||
action={action}
|
||||
showAnimation={showAnimation}
|
||||
label={name}
|
||||
prefix={picture}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ describe('PlaybookRun', () => {
|
|||
expect(ownerChip.props.user).toBe(props.owner);
|
||||
expect(ownerChip.props.onPress).toBeDefined();
|
||||
expect(ownerChip.props.teammateNameDisplay).toBe(General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME);
|
||||
expect(ownerChip.props.actionIcon).toBe(undefined);
|
||||
expect(ownerChip.props.action).toBe(undefined);
|
||||
|
||||
ownerChip.props.onPress();
|
||||
expect(openUserProfileModal).toHaveBeenCalledWith(expect.anything(), expect.anything(), {
|
||||
|
|
@ -213,7 +213,31 @@ describe('PlaybookRun', () => {
|
|||
const {getByTestId} = renderWithEverything(<PlaybookRun {...props}/>, {database});
|
||||
|
||||
const ownerChip = getByTestId('user-chip');
|
||||
expect(ownerChip).toHaveProp('actionIcon', 'downArrow');
|
||||
expect(ownerChip).toHaveProp('action', {icon: 'downArrow', onPress: expect.any(Function)});
|
||||
ownerChip.props.action.onPress();
|
||||
|
||||
expect(goToSelectUser).toHaveBeenCalledWith(
|
||||
'Owner',
|
||||
[...props.participants.map((p) => p.id), props.owner!.id],
|
||||
props.owner!.id,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(openUserProfileModal).not.toHaveBeenCalled();
|
||||
|
||||
let handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
expect(setOwner).toHaveBeenCalledWith(
|
||||
serverUrl,
|
||||
props.playbookRun!.id,
|
||||
'user-2',
|
||||
);
|
||||
expect(showPlaybookErrorSnackbar).not.toHaveBeenCalled();
|
||||
|
||||
jest.mocked(goToSelectUser).mockClear();
|
||||
jest.mocked(setOwner).mockClear();
|
||||
|
||||
// Test also pressing on the whole chip
|
||||
ownerChip.props.onPress();
|
||||
|
||||
expect(goToSelectUser).toHaveBeenCalledWith(
|
||||
|
|
@ -224,7 +248,7 @@ describe('PlaybookRun', () => {
|
|||
);
|
||||
expect(openUserProfileModal).not.toHaveBeenCalled();
|
||||
|
||||
const handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
handleSelect = jest.mocked(goToSelectUser).mock.calls[0][3];
|
||||
handleSelect(TestHelper.fakeUser({id: 'user-2'}));
|
||||
|
||||
expect(setOwner).toHaveBeenCalledWith(
|
||||
|
|
|
|||
|
|
@ -207,6 +207,13 @@ export default function PlaybookRun({
|
|||
);
|
||||
}, [handleSelectOwner, intl, owner, participants]);
|
||||
|
||||
const ownerAction = useMemo(() => {
|
||||
if (readOnly) {
|
||||
return undefined;
|
||||
}
|
||||
return {icon: 'downArrow' as const, onPress: openChangeOwnerModal};
|
||||
}, [openChangeOwnerModal, readOnly]);
|
||||
|
||||
if (!playbookRun) {
|
||||
return <ErrorState/>;
|
||||
}
|
||||
|
|
@ -255,7 +262,7 @@ export default function PlaybookRun({
|
|||
user={owner}
|
||||
onPress={readOnly ? openOwnerProfile : openChangeOwnerModal}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
actionIcon={readOnly ? undefined : 'downArrow'}
|
||||
action={ownerAction}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {buildComponent} from './utils';
|
|||
const propPossibilities = {};
|
||||
|
||||
const onPress = () => Alert.alert('Button pressed!');
|
||||
const onActionPress = () => Alert.alert('Action pressed!');
|
||||
|
||||
const ChipComponentLibrary = () => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -21,9 +22,21 @@ const ChipComponentLibrary = () => {
|
|||
const [actionIcon, actionIconPossibilities, actionIconSelector] = useDropdownProp('actionIcon', 'remove', ['remove', 'downArrow'], true);
|
||||
const [hasPrefix, hasPrefixSelector] = useBooleanProp('hasPrefix', false);
|
||||
|
||||
const actionPossibilities = useMemo(() => {
|
||||
if (!actionIconPossibilities) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
action: actionIconPossibilities.actionIcon.map((iconName) => ({
|
||||
icon: iconName,
|
||||
onPress: onActionPress,
|
||||
})),
|
||||
};
|
||||
}, [actionIconPossibilities]);
|
||||
|
||||
const components = useMemo(
|
||||
() => buildComponent(BaseChip, propPossibilities, [
|
||||
actionIconPossibilities,
|
||||
actionPossibilities,
|
||||
], [
|
||||
label,
|
||||
actionIcon,
|
||||
|
|
@ -41,7 +54,7 @@ const ChipComponentLibrary = () => {
|
|||
theme,
|
||||
},
|
||||
]),
|
||||
[label, actionIconPossibilities, actionIcon, hasPrefix.hasPrefix, theme],
|
||||
[actionPossibilities, label, actionIcon, hasPrefix.hasPrefix, theme],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function buildPropString(inputProps: {[x: string]: any}) {
|
|||
export function buildComponent(
|
||||
Component: React.ComponentType<any>,
|
||||
propPossibilities: {[x: string]: any[]},
|
||||
dropdownPossibilities: Array<{[x: string]: string[]} | undefined>,
|
||||
dropdownPossibilities: Array<{[x: string]: unknown[]} | undefined>,
|
||||
setProps: Array<{[x: string]: any} | undefined>,
|
||||
) {
|
||||
const res: React.ReactNode[] = [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue