Skip to content

Commit

Permalink
URL bar improvements (#526)
Browse files Browse the repository at this point in the history
This PR introduces several enhancements to the URL bar:
- The `home` button in expo router projects opens the root URL (`/{}`) with empty params but triggers a metro reload in projects without expo router.
Initial params may be may be introduced in future PRs.
- The `go back` button navigates back through the URL history up to a
limit of 20 URLs.
- A list of the 5 most recently visited URLs is added to the top of
`Select.Content` in the URL bar, followed by an alphabetically sorted
list of all visited URLs.
- Improved responsiveness of `Select.Content` and `Select.Trigger`
components.
- Added a scroll buttons for long URL lists in the `Select.Content`.
- Long URL names now break at dashes and display in full upon hover in
`Select.Content`.
- Added `Expo Router` as an optional dependency to the diagnostics view.

Screenshots:
![Screenshot 2024-09-04 at 11 15
34](https://github.com/user-attachments/assets/49675114-e074-42f3-9ade-9cfaf0108992)
![Screenshot 2024-09-04 at 11 15
59](https://github.com/user-attachments/assets/8d0337a3-b7c7-45d2-be23-3290ca39e35e)
![Screenshot 2024-09-03 at 13 35
04](https://github.com/user-attachments/assets/25629bfd-9fa9-4f99-87f3-7f309e7e6115)

Test Plan:
- Navigate to a non-root path and confirm that clicking the `home button` redirects to the root URL in project using Expo Router and the other without.
- Visit multiple links using both the app interface and the URL bar, then click the `go back` button to validate correct navigation through the URL history (additionally, test with dynamic links and component previews).
- Visit various links and verify that the top 5 most recent URLs are displayed in the correct order.
- Visit a lot of multiple links or component previews to test scroll functionality when the list exceeds the available space.
  • Loading branch information
p-malecki authored Sep 12, 2024
1 parent 36ad169 commit 4b1fbd5
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/vscode-extension/src/common/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export interface ProjectInterface {
getProjectState(): Promise<ProjectState>;
reload(type: ReloadAction): Promise<boolean>;
restart(forceCleanBuild: boolean): Promise<void>;
goHome(): Promise<void>;
goHome(homeUrl: string): Promise<void>;
selectDevice(deviceInfo: DeviceInfo): Promise<void>;
updatePreviewZoomLevel(zoom: ZoomLevelType): Promise<void>;

Expand Down
42 changes: 42 additions & 0 deletions packages/vscode-extension/src/dependency/DependencyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Platform } from "../utilities/platform";
const MIN_REACT_NATIVE_VERSION_SUPPORTED = "0.71.0";
const MIN_EXPO_SDK_VERSION_SUPPORTED = "49.0.0";
const MIN_STORYBOOK_VERSION_SUPPORTED = "5.2.0";
const MIN_EXPO_ROUTER_VERSION_SUPPORTED = "0.0.0";

export class DependencyManager implements Disposable {
private disposables: Disposable[] = [];
Expand Down Expand Up @@ -67,6 +68,10 @@ export class DependencyManager implements Disposable {
Logger.debug("Received checkNodeModulesInstalled command.");
this.checkNodeModulesInstalled();
return;
case "checkExpoRouterInstalled":
Logger.debug("Received checkExpoRouterInstalled command.");
this.checkExpoRouterInstalled();
return;
case "checkStorybookInstalled":
Logger.debug("Received checkStorybookInstalled command.");
this.checkStorybookInstalled();
Expand Down Expand Up @@ -315,6 +320,27 @@ export class DependencyManager implements Disposable {
return installed;
}

public async checkExpoRouterInstalled() {
const status = checkMinDependencyVersionInstalled(
"expo-router",
MIN_EXPO_ROUTER_VERSION_SUPPORTED
);

const installed = status === "installed";

this.webview.postMessage({
command: "isExpoRouterInstalled",
data: {
installed,
info: "Whether supported version of Expo Router is installed.",
error: undefined,
isOptional: !isExpoRouterProject(),
},
});
Logger.debug(`Minimum Expo version installed:`, installed);
return installed;
}

public async checkStorybookInstalled() {
const status = checkMinDependencyVersionInstalled(
"@storybook/react-native",
Expand All @@ -328,6 +354,7 @@ export class DependencyManager implements Disposable {
installed,
info: "Whether Storybook is installed.",
error: undefined,
isOptional: true,
},
});
Logger.debug("Storybook installed:", installed);
Expand Down Expand Up @@ -435,3 +462,18 @@ export function checkMinDependencyVersionInstalled(dependency: string, minVersio
export async function checkAndroidEmulatorExists() {
return fs.existsSync(EMULATOR_BINARY);
}

export function isExpoRouterProject() {
// we assume that a expo router based project contain
// the package "expo-router" in its dependencies or devDependencies
try {
const appRoot = getAppRootFolder();
const packageJson = requireNoCache(path.join(appRoot, "package.json"));
const hasExpoRouter =
Object.keys(packageJson.dependencies).some((dependency) => dependency === "expo-router") ||
Object.keys(packageJson.devDependencies).some((dependency) => dependency === "expo-router");
return hasExpoRouter;
} catch (e) {
return false;
}
}
12 changes: 10 additions & 2 deletions packages/vscode-extension/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class Project

private detectedFingerprintChange: boolean;

private expoRouterInstalled: boolean;
private storybookInstalled: boolean;

private fileWatcher: Disposable;
Expand Down Expand Up @@ -90,6 +91,7 @@ export class Project
this.trySelectingInitialDevice();
this.deviceManager.addListener("deviceRemoved", this.removeDeviceListener);
this.detectedFingerprintChange = false;
this.expoRouterInstalled = false;
this.storybookInstalled = false;

this.fileWatcher = watchProjectFiles(() => {
Expand Down Expand Up @@ -236,8 +238,12 @@ export class Project
}
}

public async goHome() {
await this.reloadMetro();
public async goHome(homeUrl: string) {
if (this.expoRouterInstalled) {
await this.openNavigation(homeUrl);
} else {
await this.reloadMetro();
}
}

//#region Session lifecycle
Expand Down Expand Up @@ -316,6 +322,8 @@ export class Project
[installNodeModules]
);

Logger.debug("Checking expo router");
this.expoRouterInstalled = await this.dependencyManager.checkExpoRouterInstalled();
Logger.debug("Checking storybook");
this.storybookInstalled = await this.dependencyManager.checkStorybookInstalled();
}
Expand Down
104 changes: 63 additions & 41 deletions packages/vscode-extension/src/webview/components/UrlBar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { useEffect, useState } from "react";
import IconButton from "./shared/IconButton";
import { ProjectInterface, ProjectState } from "../../common/Project";
import UrlSelect from "./UrlSelect";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useEffect, useState, useMemo } from "react";
import { useProject } from "../providers/ProjectProvider";
import UrlSelect, { UrlItem } from "./UrlSelect";
import { IconButtonWithOptions } from "./IconButtonWithOptions";
import IconButton from "./shared/IconButton";

interface UrlBarProps {
project: ProjectInterface;
disabled?: boolean;
}

interface ReloadButtonProps {
project: ProjectInterface;
disabled: boolean;
}

function ReloadButton({ project, disabled }: ReloadButtonProps) {
function ReloadButton({ disabled }: { disabled: boolean }) {
const { project } = useProject();
return (
<IconButtonWithOptions
onClick={() => project.restart(false)}
Expand All @@ -35,28 +25,57 @@ function ReloadButton({ project, disabled }: ReloadButtonProps) {
);
}

function UrlBar({ project, disabled }: UrlBarProps) {
const [urlList, setUrlList] = useState<{ name: string; id: string }[]>([]);
function UrlBar({ disabled }: { disabled?: boolean }) {
const { project } = useProject();

const MAX_URL_HISTORY_SIZE = 20;
const MAX_RECENT_URL_SIZE = 5;

const [backNavigationPath, setBackNavigationPath] = useState<string>("");
const [urlList, setUrlList] = useState<UrlItem[]>([]);
const [recentUrlList, setRecentUrlList] = useState<UrlItem[]>([]);
const [urlHistory, setUrlHistory] = useState<string[]>([]);

useEffect(() => {
function moveAsMostRecent(urls: UrlItem[], newUrl: UrlItem) {
return [newUrl, ...urls.filter((record) => record.id !== newUrl.id)];
}

function handleNavigationChanged(navigationData: { displayName: string; id: string }) {
const newRecord = { name: navigationData.displayName, id: navigationData.id };
setUrlList((urlList) => [
newRecord,
...urlList.filter((record) => record.id !== newRecord.id),
]);
if (backNavigationPath && backNavigationPath !== navigationData.id) {
return;
}

const newRecord: UrlItem = {
name: navigationData.displayName,
id: navigationData.id,
};
const isNotInHistory = urlHistory.length === 0 || urlHistory[0] !== newRecord.id;

setUrlList((currentUrlList) => moveAsMostRecent(currentUrlList, newRecord));
setRecentUrlList((currentRecentUrlList) => {
const updatedRecentUrls = moveAsMostRecent(currentRecentUrlList, newRecord);
return updatedRecentUrls.slice(0, MAX_RECENT_URL_SIZE);
});

if (isNotInHistory) {
setUrlHistory((currentUrlHistoryList) => {
const updatedUrlHistory = [newRecord.id, ...currentUrlHistoryList];
return updatedUrlHistory.slice(0, MAX_URL_HISTORY_SIZE);
});
}
setBackNavigationPath("");
}

project.addListener("navigationChanged", handleNavigationChanged);
const handleProjectReset = (e: ProjectState) => {
if (e.status === "starting") {
setUrlList([]);
}
};
project.addListener("projectStateChanged", handleProjectReset);
return () => {
project.removeListener("navigationChanged", handleNavigationChanged);
project.removeListener("projectStateChanged", handleProjectReset);
};
}, []);
}, [recentUrlList, urlHistory, backNavigationPath]);

const sortedUrlList = useMemo(() => {
return [...urlList].sort((a, b) => a.name.localeCompare(b.name));
}, [urlList]);

return (
<>
Expand All @@ -65,34 +84,37 @@ function UrlBar({ project, disabled }: UrlBarProps) {
label: "Go back",
side: "bottom",
}}
disabled={disabled || urlList.length < 2}
disabled={disabled || urlHistory.length < 2}
onClick={() => {
project.openNavigation(urlList[1].id);
// remove first item from the url list
setUrlList((urlList) => urlList.slice(1));
setUrlHistory((prevUrlHistory) => {
const newUrlHistory = prevUrlHistory.slice(1);
setBackNavigationPath(newUrlHistory[0]);
project.openNavigation(newUrlHistory[0]);
return newUrlHistory;
});
}}>
<span className="codicon codicon-arrow-left" />
</IconButton>
<ReloadButton project={project} disabled={disabled ?? false} />
<ReloadButton disabled={disabled ?? false} />
<IconButton
onClick={() => {
project.goHome();
setUrlList([]);
project.goHome("/{}");
}}
tooltip={{
label: "Go to main screen",
side: "bottom",
}}
disabled={disabled || urlList.length == 0}>
disabled={disabled || urlList.length < 2}>
<span className="codicon codicon-home" />
</IconButton>
<UrlSelect
onValueChange={(value: string) => {
project.openNavigation(value);
}}
items={urlList}
recentItems={recentUrlList}
items={sortedUrlList}
value={urlList[0]?.id}
disabled={disabled || urlList.length < 1}
disabled={disabled || urlList.length < 2}
/>
</>
);
Expand Down
46 changes: 38 additions & 8 deletions packages/vscode-extension/src/webview/components/UrlSelect.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

.url-select-trigger {
box-sizing: border-box;
display: inline-flex;
display: inline;
align-items: center;
padding: 0 10px;
font-size: 13px;
line-height: 1;
overflow: hidden;
cursor: pointer;
height: 36px;
border: 0px solid transparent;
Expand All @@ -18,8 +19,7 @@
color: var(--swm-url-select);
background-color: var(--swm-url-select-background);
user-select: none;
/* min-width: var(--url-select-min-width); */
max-width: var(--url-select-max-width);
min-width: var(--url-select-min-width);
}
.url-select-trigger:hover {
background-color: var(--swm-url-select-hover-background);
Expand All @@ -43,20 +43,39 @@
border-radius: 18px 18px 18px 18px;
transform: translateY(4px);
padding-bottom: 4px;
min-width: var(--url-select-min-width);
max-width: var(--url-select-max-width);
max-width: var(--radix-select-content-available-width);
max-height: var(--radix-select-content-available-height);
}

.url-select-viewport {
padding: 6px;
}

.url-select-scroll {
display: flex;
justify-content: center;
align-items: center;
}

.url-select-label {
padding: 0 4px 0 4px;
font-size: 13px;
line-height: 20px;
}

.url-select-separator {
height: 1px;
background-color: var(--swm-url-select-separator);
margin: 5px;
}

.url-select-item {
font-size: 13px;
line-height: 1;
line-height: 1.2;
display: flex;
align-items: center;
height: 30px;
min-height: 30px;
height: auto;
padding: 0 4px;
position: relative;
border-radius: 4px;
Expand All @@ -71,5 +90,16 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 240px;
max-width: calc(var(--radix-select-content-available-width) - 15px);
}
.url-select-item:hover .url-select-item-text {
white-space: normal;
padding-top: 6px;
padding-bottom: 6px;
}

@media (width <= 400px) {
.url-select-trigger {
min-width: 0;
}
}
Loading

0 comments on commit 4b1fbd5

Please sign in to comment.