Skip to content

Commit f76ed73

Browse files
feat(preview-modernization): BP AddTaskButton (#4366)
* feat(ui-uplift): Added AddTaskButtonV2 bp component * feat(ui-uplift): Added comment about the icon change * feat(preview-modernization): put changes under the ff * feat(preview-modernization): cleanup * added bp tokens to the .scss * stories * removed story for deleted componennt. * cleanup. * Update src/elements/content-sidebar/__tests__/AddTaskMenuV2.test.tsx Co-authored-by: Trevor <[email protected]> * post review fixes. * post review fixes. * removed unused import. --------- Co-authored-by: Trevor <[email protected]>
1 parent 8bf7c63 commit f76ed73

File tree

5 files changed

+229
-8
lines changed

5 files changed

+229
-8
lines changed

src/elements/content-sidebar/AddTaskButton.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import * as React from 'react';
33
import { type RouterHistory } from 'react-router-dom';
44
import { withRouterIfEnabled } from '../common/routing';
5+
import { withFeatureConsumer, getFeatureConfig } from '../common/feature-checking';
6+
import type { FeatureConfig } from '../common/feature-checking';
57

68
import AddTaskMenu from './AddTaskMenu';
9+
import AddTaskMenuV2 from './AddTaskMenuV2';
710
import TaskModal from './TaskModal';
811
import { TASK_TYPE_APPROVAL } from '../../constants';
912
import type { TaskFormProps } from './activity-feed/task-form/TaskForm';
@@ -12,6 +15,7 @@ import type { ElementsXhrError } from '../../common/types/api';
1215
import type { InternalSidebarNavigation, InternalSidebarNavigationHandler } from '../common/types/SidebarNavigation';
1316

1417
type Props = {|
18+
features: FeatureConfig,
1519
history?: RouterHistory,
1620
internalSidebarNavigation?: InternalSidebarNavigation,
1721
internalSidebarNavigationHandler?: InternalSidebarNavigationHandler,
@@ -79,16 +83,24 @@ class AddTaskButton extends React.Component<Props, State> {
7983
};
8084

8185
render() {
82-
const { isDisabled, taskFormProps } = this.props;
86+
const { features, isDisabled, taskFormProps } = this.props;
8387
const { isTaskFormOpen, taskType, error } = this.state;
88+
const featureConfig = getFeatureConfig(features, 'previewModernization');
89+
const { enabled: isPreviewModernizationEnabled } = featureConfig || {};
90+
91+
const addTaskMenuProps = {
92+
isDisabled,
93+
onMenuItemClick: this.handleClickMenuItem,
94+
setAddTaskButtonRef: this.setAddTaskButtonRef,
95+
};
8496

8597
return (
8698
<>
87-
<AddTaskMenu
88-
isDisabled={isDisabled}
89-
onMenuItemClick={this.handleClickMenuItem}
90-
setAddTaskButtonRef={this.setAddTaskButtonRef}
91-
/>
99+
{isPreviewModernizationEnabled ? (
100+
<AddTaskMenuV2 {...addTaskMenuProps} />
101+
) : (
102+
<AddTaskMenu {...addTaskMenuProps} />
103+
)}
92104
<TaskModal
93105
error={error}
94106
onSubmitError={this.handleSubmitError}
@@ -104,4 +116,4 @@ class AddTaskButton extends React.Component<Props, State> {
104116
}
105117

106118
export { AddTaskButton as AddTaskButtonComponent };
107-
export default withRouterIfEnabled(AddTaskButton);
119+
export default withFeatureConsumer(withRouterIfEnabled(AddTaskButton));
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
$bcs-AddTaskMenu-v-two-width: 256px;
2+
3+
.bcs-AddTaskMenu-v-two {
4+
padding: var(--space-2);
5+
}
6+
7+
.bcs-AddTaskMenu-v-two-menuItem {
8+
display: flex;
9+
max-width: $bcs-AddTaskMenu-v-two-width;
10+
white-space: normal;
11+
gap: var(--space-3);
12+
}
13+
14+
.bcs-AddTaskMenu-v-two-icon {
15+
align-self: center;
16+
background-color: var(--gray-05);
17+
height: var(--size-8);
18+
width: var(--size-8);
19+
border-radius: var(--radius-2);
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
flex-shrink: 0;
24+
}
25+
26+
.bcs-AddTaskMenu-v-two-title {
27+
font-weight: bold;
28+
}
29+
30+
.bcs-AddTaskMenu-v-two-description {
31+
color: var(--gray-65);
32+
font-size: var(--caption-default-font-size);
33+
line-height: var(--caption-default-line-height);
34+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as React from 'react';
2+
import { useIntl } from 'react-intl';
3+
4+
import { DropdownMenu, TriggerButton } from '@box/blueprint-web';
5+
import { ApprovalTask } from '@box/blueprint-web-assets/icons/Fill';
6+
import { Tasks } from '@box/blueprint-web-assets/icons/MediumFilled';
7+
import messages from './messages';
8+
import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../constants';
9+
import type { TaskType } from '../../common/types/tasks';
10+
11+
import './AddTaskMenuV2.scss';
12+
13+
export interface AddTaskMenuV2Props {
14+
isDisabled: boolean;
15+
onMenuItemClick: (taskType: TaskType) => void;
16+
setAddTaskButtonRef?: (element: HTMLButtonElement | null) => void;
17+
}
18+
19+
const AddTaskMenuV2: React.FC<AddTaskMenuV2Props> = ({ isDisabled, onMenuItemClick, setAddTaskButtonRef }) => {
20+
const { formatMessage } = useIntl();
21+
22+
const [isOpen, setIsOpen] = React.useState(false);
23+
24+
const handleMenuItemClick = React.useCallback(
25+
(taskType: TaskType) => {
26+
// Open the modal first
27+
onMenuItemClick(taskType);
28+
// Then close the dropdown. We rely on onCloseAutoFocus to prevent focus restoration.
29+
setIsOpen(false);
30+
},
31+
[onMenuItemClick],
32+
);
33+
34+
return (
35+
<DropdownMenu.Root open={isOpen} onOpenChange={setIsOpen}>
36+
<DropdownMenu.Trigger>
37+
<TriggerButton
38+
variant="secondary"
39+
disabled={isDisabled}
40+
ref={setAddTaskButtonRef}
41+
caretDirection={isOpen ? 'up' : 'down'}
42+
label={formatMessage(messages.tasksAddTask)}
43+
/>
44+
</DropdownMenu.Trigger>
45+
46+
<DropdownMenu.Content
47+
align="end"
48+
className="bcs-AddTaskMenu-v-two"
49+
onCloseAutoFocus={(event: Event) => {
50+
// Prevent focus from returning to the trigger button when the menu closes.
51+
// This allows the Modal (which was just opened) to keep focus on its input field
52+
// without having it stolen back, which would trigger a blur validation error.
53+
event.preventDefault();
54+
}}
55+
>
56+
<DropdownMenu.Item onClick={() => handleMenuItemClick(TASK_TYPE_GENERAL)}>
57+
<div className="bcs-AddTaskMenu-v-two-menuItem">
58+
<div className="bcs-AddTaskMenu-v-two-icon">
59+
<Tasks color="black" width={20} height={20} />
60+
</div>
61+
<div>
62+
<div className="bcs-AddTaskMenu-v-two-title">
63+
{formatMessage(messages.taskAddTaskGeneral)}
64+
</div>
65+
<div className="bcs-AddTaskMenu-v-two-description">
66+
{formatMessage(messages.taskAddTaskGeneralDescription)}
67+
</div>
68+
</div>
69+
</div>
70+
</DropdownMenu.Item>
71+
<DropdownMenu.Item onClick={() => handleMenuItemClick(TASK_TYPE_APPROVAL)}>
72+
<div className="bcs-AddTaskMenu-v-two-menuItem">
73+
<div className="bcs-AddTaskMenu-v-two-icon">
74+
{/* Should be replaced by icons/MediumFilled/ApprovalTask after it will be availabel */}
75+
<ApprovalTask color="black" width={20} height={20} />
76+
</div>
77+
<div>
78+
<div className="bcs-AddTaskMenu-v-two-title">
79+
{formatMessage(messages.taskAddTaskApproval)}
80+
</div>
81+
<div className="bcs-AddTaskMenu-v-two-description">
82+
{formatMessage(messages.taskAddTaskApprovalDescription)}
83+
</div>
84+
</div>
85+
</div>
86+
</DropdownMenu.Item>
87+
</DropdownMenu.Content>
88+
</DropdownMenu.Root>
89+
);
90+
};
91+
92+
export default AddTaskMenuV2;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as React from 'react';
2+
import { IntlShape } from 'react-intl';
3+
import { render, screen, userEvent } from '../../../test-utils/testing-library';
4+
import AddTaskMenuV2 from '../AddTaskMenuV2';
5+
import { TASK_TYPE_APPROVAL, TASK_TYPE_GENERAL } from '../../../constants';
6+
7+
describe('elements/content-sidebar/AddTaskMenuV2', () => {
8+
const defaultProps = {
9+
isDisabled: false,
10+
onMenuItemClick: jest.fn(),
11+
setAddTaskButtonRef: jest.fn(),
12+
intl: {
13+
formatMessage: jest.fn(msg => msg.defaultMessage || msg.id),
14+
} as unknown as IntlShape,
15+
};
16+
17+
const renderComponent = (props = {}) => {
18+
return render(<AddTaskMenuV2 {...defaultProps} {...props} />);
19+
};
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
test('should render trigger button with correct props', () => {
26+
renderComponent();
27+
28+
const triggerButton = screen.getByRole('button', { name: /add task/i });
29+
expect(triggerButton).toBeInTheDocument();
30+
expect(triggerButton).not.toBeDisabled();
31+
});
32+
33+
test('should render disabled trigger button when isDisabled is true', () => {
34+
renderComponent({ isDisabled: true });
35+
36+
const triggerButton = screen.getByRole('button', { name: /add task/i });
37+
expect(triggerButton).toBeDisabled();
38+
});
39+
40+
test('should call onMenuItemClick with TASK_TYPE_GENERAL when general task item is clicked', async () => {
41+
const onMenuItemClick = jest.fn();
42+
const user = userEvent();
43+
44+
renderComponent({ onMenuItemClick });
45+
46+
// Open the dropdown
47+
const triggerButton = screen.getByRole('button', { name: /add task/i });
48+
await user.click(triggerButton);
49+
50+
// Click on general task menu item
51+
const generalTaskItem = screen.getByRole('menuitem', { name: /general task/i });
52+
await user.click(generalTaskItem);
53+
54+
expect(onMenuItemClick).toHaveBeenCalledTimes(1);
55+
expect(onMenuItemClick).toHaveBeenCalledWith(TASK_TYPE_GENERAL);
56+
});
57+
58+
test('should call onMenuItemClick with TASK_TYPE_APPROVAL when approval task item is clicked', async () => {
59+
const onMenuItemClick = jest.fn();
60+
const user = userEvent();
61+
62+
renderComponent({ onMenuItemClick });
63+
64+
// Open the dropdown
65+
const triggerButton = screen.getByRole('button', { name: /add task/i });
66+
await user.click(triggerButton);
67+
68+
// Click on approval task menu item
69+
const approvalTaskItem = screen.getByRole('menuitem', { name: /approval task/i });
70+
await user.click(approvalTaskItem);
71+
72+
expect(onMenuItemClick).toHaveBeenCalledTimes(1);
73+
expect(onMenuItemClick).toHaveBeenCalledWith(TASK_TYPE_APPROVAL);
74+
});
75+
76+
test('should call setAddTaskButtonRef with button element', () => {
77+
const setAddTaskButtonRef = jest.fn();
78+
79+
renderComponent({ setAddTaskButtonRef });
80+
81+
expect(setAddTaskButtonRef).toHaveBeenCalled();
82+
});
83+
});

src/elements/content-sidebar/__tests__/__snapshots__/ActivitySidebar.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`elements/content-sidebar/ActivitySidebar render() should render the act
44
<SidebarContent
55
actions={
66
<React.Fragment>
7-
<withRouterIfEnabled(AddTaskButton)
7+
<ForwardRef(withFeatureConsumer(withRouterIfEnabled(AddTaskButton)))
88
isDisabled={false}
99
onTaskModalClose={[Function]}
1010
taskFormProps={

0 commit comments

Comments
 (0)