diff --git a/i18n/en-US.properties b/i18n/en-US.properties
index 2289c55bb6..5ec5d363d6 100644
--- a/i18n/en-US.properties
+++ b/i18n/en-US.properties
@@ -646,6 +646,8 @@ be.rootBreadcrumb = All Files
be.save = Save
# Shown as the title in the sub header while searching.
be.searchBreadcrumb = Search Results
+# Aria label for the clear button in the search box.
+be.searchClear = Clear search
# Shown as a placeholder in the search box.
be.searchPlaceholder = Search files and folders
# Default label for selected items list in the footer.
diff --git a/src/elements/common/__mocks__/mockRecentItems.ts b/src/elements/common/__mocks__/mockRecentItems.ts
new file mode 100644
index 0000000000..8b4c146fc8
--- /dev/null
+++ b/src/elements/common/__mocks__/mockRecentItems.ts
@@ -0,0 +1,238 @@
+const mockRecentItems = {
+ next_marker: '',
+ limit: 100,
+ order: {
+ by: 'interacted_at',
+ direction: 'DESC',
+ },
+ entries: [
+ {
+ type: 'recent_item',
+ interaction_type: 'item_preview',
+ interacted_at: '2025-01-31T13:40:24-08:00',
+ item: {
+ type: 'file',
+ id: '1222291629977',
+ etag: '1',
+ name: 'Resume (4).pdf',
+ size: 69266,
+ parent: {
+ type: 'folder',
+ id: '0',
+ sequence_id: null,
+ etag: null,
+ name: 'All Files',
+ },
+ extension: 'pdf',
+ permissions: {
+ can_download: true,
+ can_preview: true,
+ can_upload: true,
+ can_comment: true,
+ can_rename: true,
+ can_delete: true,
+ can_share: true,
+ can_set_share_access: true,
+ can_invite_collaborator: true,
+ can_annotate: true,
+ can_view_annotations_all: true,
+ can_view_annotations_self: true,
+ can_create_annotations: true,
+ can_view_annotations: true,
+ },
+ path_collection: {
+ total_count: 1,
+ entries: [
+ {
+ type: 'folder',
+ id: '0',
+ sequence_id: null,
+ etag: null,
+ name: 'All Files',
+ },
+ ],
+ },
+ modified_at: '2023-05-24T11:43:41-07:00',
+ created_at: '2023-05-24T11:43:41-07:00',
+ modified_by: {
+ type: 'user',
+ id: '24991742828',
+ name: 'GW',
+ login: 'gw@boxdemo.com',
+ },
+ has_collaborations: false,
+ is_externally_owned: false,
+ authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/1222291629977/content',
+ is_download_available: true,
+ representations: {
+ entries: [
+ {
+ representation: 'jpg',
+ properties: {
+ dimensions: '1024x1024',
+ paged: 'false',
+ thumb: 'false',
+ },
+ info: {
+ url: 'https://api.box.com/2.0/internal_files/1222291629977/versions/1334003366777/representations/jpg_1024x1024',
+ },
+ status: {
+ state: 'success',
+ },
+ content: {
+ url_template:
+ 'https://dl.boxcloud.com/api/2.0/internal_files/1222291629977/versions/1334003366777/representations/jpg_1024x1024/content/{+asset_path}',
+ },
+ },
+ ],
+ },
+ file_version: {
+ type: 'file_version',
+ id: '1334003366777',
+ sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59',
+ },
+ sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59',
+ shared_link: {
+ url: 'https://app.box.com/s/6wvvmdmgmo9ky62p054zbtgn1ph78els',
+ download_url: 'https://app.box.com/shared/static/6wvvmdmgmo9ky62p054zbtgn1ph78els.pdf',
+ vanity_url: null,
+ vanity_name: null,
+ effective_access: 'open',
+ effective_permission: 'can_download',
+ is_password_enabled: false,
+ unshared_at: null,
+ download_count: 0,
+ preview_count: 0,
+ access: 'open',
+ permissions: {
+ can_preview: true,
+ can_download: true,
+ can_edit: false,
+ },
+ },
+ watermark_info: {
+ is_watermarked: false,
+ },
+ },
+ interaction_shared_link: null,
+ },
+ {
+ type: 'recent_item',
+ interaction_type: 'item_preview',
+ interacted_at: '2025-01-28T11:12:18-08:00',
+ item: {
+ type: 'file',
+ id: '1318276254035',
+ etag: '1',
+ name: 'Resume (2) (1).pdf',
+ size: 69266,
+ parent: {
+ type: 'folder',
+ id: '218662304788',
+ sequence_id: '0',
+ etag: '0',
+ name: 'metadataquery',
+ },
+ extension: 'pdf',
+ permissions: {
+ can_download: true,
+ can_preview: true,
+ can_upload: true,
+ can_comment: true,
+ can_rename: true,
+ can_delete: true,
+ can_share: true,
+ can_set_share_access: true,
+ can_invite_collaborator: true,
+ can_annotate: true,
+ can_view_annotations_all: true,
+ can_view_annotations_self: true,
+ can_create_annotations: true,
+ can_view_annotations: true,
+ },
+ path_collection: {
+ total_count: 2,
+ entries: [
+ {
+ type: 'folder',
+ id: '0',
+ sequence_id: null,
+ etag: null,
+ name: 'All Files',
+ },
+ {
+ type: 'folder',
+ id: '218662304788',
+ sequence_id: '0',
+ etag: '0',
+ name: 'metadataquery',
+ },
+ ],
+ },
+ modified_at: '2023-09-26T14:04:52-07:00',
+ created_at: '2023-09-26T14:04:52-07:00',
+ modified_by: {
+ type: 'user',
+ id: '24991742828',
+ name: 'GW',
+ login: 'gw@boxdemo.com',
+ },
+ has_collaborations: false,
+ is_externally_owned: false,
+ authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/1318276254035/content',
+ is_download_available: true,
+ representations: {
+ entries: [
+ {
+ representation: 'jpg',
+ properties: {
+ dimensions: '1024x1024',
+ paged: 'false',
+ thumb: 'false',
+ },
+ info: {
+ url: 'https://api.box.com/2.0/internal_files/1318276254035/versions/1442246185235/representations/jpg_1024x1024',
+ },
+ status: {
+ state: 'success',
+ },
+ content: {
+ url_template:
+ 'https://dl.boxcloud.com/api/2.0/internal_files/1318276254035/versions/1442246185235/representations/jpg_1024x1024/content/{+asset_path}',
+ },
+ },
+ ],
+ },
+ file_version: {
+ type: 'file_version',
+ id: '1442246185235',
+ sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59',
+ },
+ sha1: 'c60c18bcdc28ed573f7acb440e5d5f1ca9bd2f59',
+ shared_link: {
+ url: 'https://app.box.com/s/4isrn71f96h1q76dluj1a71h6o8d6z17',
+ download_url: 'https://app.box.com/shared/static/4isrn71f96h1q76dluj1a71h6o8d6z17.pdf',
+ vanity_url: null,
+ vanity_name: null,
+ effective_access: 'open',
+ effective_permission: 'can_download',
+ is_password_enabled: false,
+ unshared_at: null,
+ download_count: 0,
+ preview_count: 0,
+ access: 'open',
+ permissions: {
+ can_preview: true,
+ can_download: true,
+ can_edit: false,
+ },
+ },
+ watermark_info: {
+ is_watermarked: false,
+ },
+ },
+ interaction_shared_link: null,
+ },
+ ],
+};
+export default mockRecentItems;
diff --git a/src/elements/common/header/Header.js b/src/elements/common/header/Header.js
deleted file mode 100644
index 412c043674..0000000000
--- a/src/elements/common/header/Header.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * @flow
- * @file Header bar
- * @author Box
- */
-
-import * as React from 'react';
-import { injectIntl } from 'react-intl';
-import Logo from './Logo';
-import messages from '../messages';
-import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants';
-import type { View } from '../../../common/types/core';
-
-import './Header.scss';
-
-type Props = {
- intl: any,
- isHeaderLogoVisible?: boolean,
- isSmall: boolean,
- logoUrl?: string,
- onSearch: Function,
- searchQuery: string,
- view: View,
-};
-
-// eslint-disable-next-line react/prop-types
-const Header = ({ isHeaderLogoVisible = true, view, isSmall, searchQuery, onSearch, logoUrl, intl }: Props) => {
- const { formatMessage } = intl;
- const search = ({ currentTarget }: { currentTarget: HTMLInputElement }) => onSearch(currentTarget.value);
- const searchMessage = formatMessage(messages.searchPlaceholder);
- const isFolder = view === VIEW_FOLDER;
- const isSearch = view === VIEW_SEARCH;
-
- return (
-
- {isHeaderLogoVisible &&
}
-
-
-
-
- );
-};
-
-export { Header as HeaderBase };
-export default injectIntl(Header);
diff --git a/src/elements/common/header/Header.js.flow b/src/elements/common/header/Header.js.flow
new file mode 100644
index 0000000000..232dc0daf2
--- /dev/null
+++ b/src/elements/common/header/Header.js.flow
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { SearchInput } from '@box/blueprint-web';
+import Logo from './Logo';
+import messages from '../messages';
+import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants';
+import type { View } from '../../../common/types/core';
+
+import './Header.scss';
+
+type HeaderProps = {
+ isHeaderLogoVisible?: boolean,
+ logoUrl?: string,
+ onSearch: any,
+ view: View
+};
+
+const Header = ({
+ isHeaderLogoVisible = true,
+ view,
+ onSearch,
+ logoUrl,
+}: HeaderProps) => {
+ const { formatMessage } = useIntl();
+ const searchMessage = formatMessage(messages.searchPlaceholder);
+ const isFolder = view === VIEW_FOLDER;
+ const isSearch = view === VIEW_SEARCH;
+
+ return (
+
+ {isHeaderLogoVisible &&
}
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/elements/common/header/Header.scss b/src/elements/common/header/Header.scss
index 174ca91ac6..e41e428d19 100644
--- a/src/elements/common/header/Header.scss
+++ b/src/elements/common/header/Header.scss
@@ -5,8 +5,7 @@
flex: 0 0 70px;
align-items: center;
padding: 0 25px 0 0;
- background: $almost-white;
- border-bottom: 1px solid $bdl-gray-10;
+ border-bottom: 1px solid var(--border-divider-border);
.be-is-small & {
padding-right: 20px;
@@ -14,6 +13,7 @@
.be-search {
flex: 1;
+ max-width: 520px;
padding-left: 20px;
}
diff --git a/src/elements/common/header/Header.tsx b/src/elements/common/header/Header.tsx
new file mode 100644
index 0000000000..5f0451aeb9
--- /dev/null
+++ b/src/elements/common/header/Header.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { useIntl } from 'react-intl';
+import { SearchInput } from '@box/blueprint-web';
+import Logo from './Logo';
+import messages from '../messages';
+import { VIEW_FOLDER, VIEW_SEARCH } from '../../../constants';
+import type { View } from '../../../common/types/core';
+
+import './Header.scss';
+
+export interface HeaderProps {
+ isHeaderLogoVisible?: boolean;
+ logoUrl?: string;
+ onSearch: (value: string) => void;
+ view: View;
+}
+
+const Header = ({ isHeaderLogoVisible = true, logoUrl, onSearch, view }: HeaderProps) => {
+ const { formatMessage } = useIntl();
+ const searchMessage = formatMessage(messages.searchPlaceholder);
+ const isFolder = view === VIEW_FOLDER;
+ const isSearch = view === VIEW_SEARCH;
+
+ return (
+
+ {isHeaderLogoVisible &&
}
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/src/elements/common/header/Logo.js b/src/elements/common/header/Logo.js.flow
similarity index 85%
rename from src/elements/common/header/Logo.js
rename to src/elements/common/header/Logo.js.flow
index cf073f539e..edd09a2fd2 100644
--- a/src/elements/common/header/Logo.js
+++ b/src/elements/common/header/Logo.js.flow
@@ -1,9 +1,3 @@
-/**
- * @flow
- * @file Logo for the header
- * @author Box
- */
-
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import IconLogo from '../../../icons/general/IconLogo';
@@ -11,7 +5,7 @@ import messages from '../messages';
import './Logo.scss';
type Props = {
- url?: string,
+ url?: string
};
function getLogo(url?: string) {
@@ -29,7 +23,9 @@ function getLogo(url?: string) {
);
}
-const Logo = ({ url }: Props) => (
+const Logo = ({
+ url,
+}: Props) => (
{getLogo(url)}
diff --git a/src/elements/common/header/Logo.scss b/src/elements/common/header/Logo.scss
index 2d16fa03e8..21adf093e2 100644
--- a/src/elements/common/header/Logo.scss
+++ b/src/elements/common/header/Logo.scss
@@ -4,8 +4,8 @@
padding-left: 20px;
.be-logo-custom {
- max-width: 80px;
- max-height: 32px;
+ max-width: 240px;
+ max-height: 40px;
.be-is-small & {
max-width: 75px;
@@ -16,9 +16,9 @@
display: flex;
align-items: center;
justify-content: center;
- width: 75px;
- height: 32px;
- background-color: $bdl-gray-10;
+ width: 208px;
+ height: 48px;
+ background-color: var(--gray-10);
border: 1px dashed;
.be-is-small & {
diff --git a/src/elements/common/header/Logo.tsx b/src/elements/common/header/Logo.tsx
new file mode 100644
index 0000000000..257c299ddf
--- /dev/null
+++ b/src/elements/common/header/Logo.tsx
@@ -0,0 +1,33 @@
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { BoxLogo } from '@box/blueprint-web-assets/icons/Logo';
+import messages from '../messages';
+import './Logo.scss';
+
+export interface LogoProps {
+ url?: string;
+}
+
+function getLogo(url?: string) {
+ if (url === 'box') {
+ return ;
+ }
+
+ if (typeof url === 'string') {
+ return
;
+ }
+
+ return (
+
+
+
+ );
+}
+
+const Logo = ({ url }: LogoProps) => (
+
+ {getLogo(url)}
+
+);
+
+export default Logo;
diff --git a/src/elements/common/header/__tests__/Header.test.js b/src/elements/common/header/__tests__/Header.test.js
deleted file mode 100644
index 814b01508a..0000000000
--- a/src/elements/common/header/__tests__/Header.test.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import * as React from 'react';
-import { render, screen } from '@testing-library/react';
-import { HeaderBase as Header } from '../Header';
-
-describe('elements/common/header/Header', () => {
- const intl = {
- formatMessage: jest.fn().mockImplementation(message => message.defaultMessage),
- };
-
- const renderComponent = props => render();
-
- test('renders Logo component when isHeaderLogoVisible is `true`', () => {
- renderComponent({ isHeaderLogoVisible: true });
- expect(screen.getByTestId('be-Logo')).toBeInTheDocument();
- });
-
- test('does not render Logo component when isHeaderLogoVisible is `false`', () => {
- renderComponent({ isHeaderLogoVisible: false });
- expect(screen.queryByTestId('be-Logo')).not.toBeInTheDocument();
- });
-
- test('renders matching values for aria-label and placeholder attributes', () => {
- renderComponent();
- const searchInput = screen.getByTestId('be-Header-searchInput');
- const searchMessage = 'Search files and folders';
-
- expect(searchInput.getAttribute('aria-label')).toBe(searchMessage);
- expect(searchInput.getAttribute('placeholder')).toBe(searchMessage);
- });
-
- test('disables search input when view is not `folder` and not `search`', () => {
- renderComponent({ view: 'recents' });
- expect(screen.getByTestId('be-Header-searchInput')).toBeDisabled();
- });
-
- test.each(['folder', 'search'])('does not disable search input when view is %s', view => {
- renderComponent({ view });
- expect(screen.getByTestId('be-Header-searchInput')).not.toBeDisabled();
- });
-});
diff --git a/src/elements/common/header/__tests__/Header.test.tsx b/src/elements/common/header/__tests__/Header.test.tsx
new file mode 100644
index 0000000000..3a14166ece
--- /dev/null
+++ b/src/elements/common/header/__tests__/Header.test.tsx
@@ -0,0 +1,63 @@
+import * as React from 'react';
+import { userEvent } from '@testing-library/user-event';
+import { render, screen } from '../../../../test-utils/testing-library';
+import Header, { HeaderProps } from '../Header';
+
+jest.mock('@box/blueprint-web-assets/icons/Logo', () => {
+ return {
+ BoxLogo: () => BoxLogo
,
+ };
+});
+
+describe('elements/common/header/Header', () => {
+ const renderComponent = (props?: Partial) =>
+ render();
+
+ test('disables search input when view is not `folder` and not `search`', () => {
+ renderComponent({ view: 'recents' });
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument();
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeDisabled();
+ });
+
+ test.each(['folder', 'search'])('does not disable search input when view is %s', view => {
+ renderComponent({ view });
+
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument();
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).not.toBeDisabled();
+ });
+
+ test('onSearch is called when search input changes', async () => {
+ const onSearch = jest.fn();
+ renderComponent({ onSearch });
+ const searchInput = screen.getByRole('searchbox', { name: 'Search files and folders' });
+
+ expect(onSearch).not.toHaveBeenCalled();
+ await userEvent.type(searchInput, 'test');
+ expect(onSearch).toHaveBeenCalled();
+ });
+
+ describe('Logo', () => {
+ test('renders Logo component when isHeaderLogoVisible is `true`', () => {
+ renderComponent({ isHeaderLogoVisible: true });
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument();
+
+ expect(screen.getByText('Logo')).toBeInTheDocument();
+ });
+
+ test('does not render Logo component when isHeaderLogoVisible is `false`', () => {
+ renderComponent({ isHeaderLogoVisible: false });
+ expect(screen.queryByText('Logo')).not.toBeInTheDocument();
+ expect(screen.getByRole('searchbox', { name: 'Search files and folders' })).toBeInTheDocument();
+ });
+
+ test('renders BoxLogo component when logoUrl is `box`', () => {
+ renderComponent({ logoUrl: 'box' });
+ expect(screen.getByText('BoxLogo')).toBeInTheDocument();
+ });
+
+ test('renders custom logo component when logoUrl is a string', () => {
+ renderComponent({ logoUrl: 'https://example.com/logo.png' });
+ expect(screen.getByRole('presentation')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/elements/common/header/index.js b/src/elements/common/header/index.js.flow
similarity index 80%
rename from src/elements/common/header/index.js
rename to src/elements/common/header/index.js.flow
index 31c1b1dcee..579f1ac23f 100644
--- a/src/elements/common/header/index.js
+++ b/src/elements/common/header/index.js.flow
@@ -1,2 +1 @@
-// @flow
export { default } from './Header';
diff --git a/src/elements/common/header/index.ts b/src/elements/common/header/index.ts
new file mode 100644
index 0000000000..579f1ac23f
--- /dev/null
+++ b/src/elements/common/header/index.ts
@@ -0,0 +1 @@
+export { default } from './Header';
diff --git a/src/elements/common/messages.js b/src/elements/common/messages.js
index 62ec32afd7..140acf9ee2 100644
--- a/src/elements/common/messages.js
+++ b/src/elements/common/messages.js
@@ -377,6 +377,11 @@ const messages = defineMessages({
description: 'Dropdown select option to remove access.',
defaultMessage: 'Remove shared link',
},
+ searchClear: {
+ id: 'be.searchClear',
+ description: 'Aria label for the clear button in the search box.',
+ defaultMessage: 'Clear search',
+ },
searchPlaceholder: {
id: 'be.searchPlaceholder',
description: 'Shown as a placeholder in the search box.',
diff --git a/src/elements/common/sub-header/SubHeader.scss b/src/elements/common/sub-header/SubHeader.scss
index 3f249854ac..d8a3bc8bc0 100644
--- a/src/elements/common/sub-header/SubHeader.scss
+++ b/src/elements/common/sub-header/SubHeader.scss
@@ -6,7 +6,7 @@
align-items: center;
justify-content: space-between;
padding: 0 20px 0 25px;
- border-bottom: 1px solid $bdl-gray-10;
+ border-bottom: 1px solid var(--border-divider-border);
box-shadow: 0 4px 6px -2px $transparent-black;
.be-is-small & {
diff --git a/src/elements/content-explorer/ContentExplorer.js b/src/elements/content-explorer/ContentExplorer.js
index 41c4848983..79259cc4da 100644
--- a/src/elements/content-explorer/ContentExplorer.js
+++ b/src/elements/content-explorer/ContentExplorer.js
@@ -1643,7 +1643,6 @@ class ContentExplorer extends Component {
isUploadModalOpen,
markers,
rootName,
- searchQuery,
selected,
view,
}: State = this.state;
@@ -1672,13 +1671,7 @@ class ContentExplorer extends Component {
{!isDefaultViewMetadata && (
<>
-
+
{
return new HttpResponse('Internal Server Error', { status: 500 });
}),
+ http.get(`${DEFAULT_HOSTNAME_API}/2.0/recent_items`, () => {
+ return HttpResponse.json(mockRecentItems);
+ }),
],
},
},
diff --git a/src/elements/content-picker/ContentPicker.js b/src/elements/content-picker/ContentPicker.js
index f7966c892e..892b1c043b 100644
--- a/src/elements/content-picker/ContentPicker.js
+++ b/src/elements/content-picker/ContentPicker.js
@@ -1244,7 +1244,6 @@ class ContentPicker extends Component {