Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions lib/dev-pages-utils/__tests__/app-modes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { describe, expect, test, vi } from "vitest";

import { Density, Mode } from "@cloudscape-design/global-styles";

import { appModesDefaults, formatAppModes, parseAppModes, updateAppModes } from "../app-modes";

describe("parseAppModes", () => {
test("casts boolean strings and applies defaults for absent params", () => {
const params = parseAppModes(new URLSearchParams("mode=dark&motionDisabled=true"));
expect(params.mode).toBe("dark");
expect(params.motionDisabled).toBe(true);
expect(params.density).toBe("comfortable"); // default
expect(params.i18n).toBe(true); // default
});

test("keeps a valid theme", () => {
expect(parseAppModes(new URLSearchParams("theme=one-theme")).theme).toBe("one-theme");
});

test("leaves theme undefined when absent", () => {
expect(parseAppModes(new URLSearchParams("")).theme).toBeUndefined();
});
});

describe("formatAppModes", () => {
test("serializes every param to a string (full URL, no omission)", () => {
expect(formatAppModes(appModesDefaults)).toEqual({
mode: "light",
density: "comfortable",
direction: "ltr",
motionDisabled: "false",
i18n: "true",
screenshotMode: "false",
});
});

test("skips undefined values", () => {
expect(formatAppModes({ mode: Mode.Dark, appLayoutToolbar: undefined })).toEqual({ mode: "dark" });
});
});

describe("updateAppModes", () => {
test("writes the full serialized query through setQuery", () => {
const setQuery = vi.fn();
updateAppModes(appModesDefaults, { mode: Mode.Dark }, setQuery);
expect(setQuery).toHaveBeenCalledWith({
mode: "dark",
density: "comfortable",
direction: "ltr",
motionDisabled: "false",
i18n: "true",
screenshotMode: "false",
});
});

test("does not reload when direction is unchanged", () => {
const reload = vi.fn();
vi.stubGlobal("window", { location: { reload } });
updateAppModes(appModesDefaults, { mode: Mode.Dark }, vi.fn());
expect(reload).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});

test("reloads when direction changes via next", () => {
const reload = vi.fn();
vi.stubGlobal("window", { location: { reload } });
updateAppModes(appModesDefaults, { direction: "rtl" }, vi.fn());
expect(reload).toHaveBeenCalledOnce();
vi.unstubAllGlobals();
});

test("does not reload when next omits direction even if current is non-default", () => {
const reload = vi.fn();
vi.stubGlobal("window", { location: { reload } });
updateAppModes({ ...appModesDefaults, direction: "rtl" }, { density: Density.Compact }, vi.fn());
expect(reload).not.toHaveBeenCalled();
vi.unstubAllGlobals();
});
});
37 changes: 37 additions & 0 deletions lib/dev-pages-utils/app-modes-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo } from "react";
import { useSearchParams } from "react-router-dom";

import { AppContextType, appModesDefaults, applyAppModes, AppUrlParams, parseAppModes, updateAppModes } from "./app-modes.js";

const AppModesContext = createContext<AppContextType>({
urlParams: appModesDefaults,
setUrlParams: () => {},
});

export function AppModesProvider({ children }: { children: ReactNode }) {
const [searchParams, setSearchParams] = useSearchParams();

// Depend on the whole object (memoized on the query) rather than enumerating params, so params
// added to parseAppModes/applyAppModes re-apply without touching this file or its consumers.
const urlParams = useMemo(() => parseAppModes(searchParams), [searchParams]);

const setUrlParams = useCallback(
(newParams: Partial<AppUrlParams>) => updateAppModes(urlParams, newParams, setSearchParams),
[urlParams, setSearchParams]
);

useEffect(() => {
applyAppModes(urlParams);
}, [urlParams]);

const value = useMemo<AppContextType>(() => ({ urlParams, setUrlParams }), [urlParams, setUrlParams]);

return <AppModesContext.Provider value={value}>{children}</AppModesContext.Provider>;
}

// Pass a type argument for package-specific params, e.g. useAppModes<{ myFlag: boolean }>().
export function useAppModes<T = unknown>(): AppContextType<T> {
return useContext(AppModesContext) as AppContextType<T>;
}
80 changes: 80 additions & 0 deletions lib/dev-pages-utils/app-modes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {
applyDensity,
applyMode,
applyTheme,
disableMotion,
Density,
Mode,
Theme,
} from "@cloudscape-design/global-styles";
import mapValues from "lodash/mapValues";

export interface AppUrlParams {
mode: Mode;
density: Density;
direction: "ltr" | "rtl";
motionDisabled: boolean;
theme?: Theme;
i18n?: boolean;
screenshotMode?: boolean;
}

export interface AppContextType<T = unknown> {
pageId?: string;
urlParams: AppUrlParams & T;
setUrlParams: (newParams: Partial<AppUrlParams & T>) => void;
}

export const appModesDefaults: AppUrlParams = {
mode: Mode.Light,
density: Density.Comfortable,
direction: "ltr",
motionDisabled: false,
i18n: true,
screenshotMode: false,
};

export function parseAppModes(searchParams: URLSearchParams): AppUrlParams {
const queryParams: Record<string, any> = { ...appModesDefaults };
searchParams.forEach((value, key) => (queryParams[key] = value));

return mapValues(queryParams, value => {
if (value === "true" || value === "false") {
return value === "true";
}
return value;
}) as AppUrlParams;
}

export function formatAppModes<T extends object>(params: T): Record<string, string> {
const query: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
if (value === undefined) continue;
query[key] = String(value);
}
return query;
}

// Reloads on direction change: some components read the document `dir` only at mount.
export function updateAppModes(
current: AppUrlParams,
next: Partial<AppUrlParams>,
setQuery: (query: Record<string, string>) => void,
): void {
const merged = { ...current, ...next };
setQuery(formatAppModes(merged));
if ((next.direction ?? current.direction) !== current.direction) {
window.location.reload();
}
}

export function applyAppModes(params: AppUrlParams, target: Element = document.body): void {
applyMode(params.mode, target);
applyDensity(params.density, target);
disableMotion(params.motionDisabled, target);
applyTheme(params.theme ?? null, target);
document.documentElement.setAttribute("dir", params.direction);
}
2 changes: 2 additions & 0 deletions lib/dev-pages-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
import { PermutationsView, PermutationsViewProps } from "./permutations-view.js";
import { createPermutations, ComponentPermutations } from "./permutations.js";

export { type AppUrlParams } from "./app-modes.js";
export { AppModesProvider, useAppModes } from "./app-modes-provider.js";
export { createPermutations, PermutationsView, type ComponentPermutations, type PermutationsViewProps };
92 changes: 89 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading