Skip to content

Commit

Permalink
Merge f758ab5 into f5a0e44
Browse files Browse the repository at this point in the history
  • Loading branch information
nhardy committed Aug 23, 2023
2 parents f5a0e44 + f758ab5 commit 2959ec6
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/kerosene-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@kablamo/kerosene-ui",
"version": "0.0.33",
"version": "0.0.34",
"repository": "https://github.com/KablamoOSS/kerosene/tree/master/packages/kerosene-ui",
"bugs": {
"url": "https://github.com/KablamoOSS/kerosene/issues"
Expand Down
10 changes: 10 additions & 0 deletions packages/kerosene-ui/readme.md
Expand Up @@ -14,6 +14,10 @@ Context Provider for the CurrentTimeEmitter used internally by the `useCurrentTi

### `<ShowWhen />`

### `<TimeZoneProvider />`

Context Provider for the `useTimeZone` hook. May be used to override the default `"Etc/UTC"` `timeZone` value during SSR and hydration.

## Utility Types

### `UnwrapComponent<T>`
Expand Down Expand Up @@ -96,6 +100,12 @@ Custom hook which allows reading/writing of `sessionStorage` in a manner similar

Custom hook which provides a stable identity between renders for `value` which is equal to the previous value according to the `isEqual` function.

### `useTimeZone()`

Custom hook which returns the current `timeZone`.

Defaults to `"Etc/UTC"` during SSR and hydration, but this may be overriden with a provider `<TimeZoneProvider ssrTimeZone={timeZone}>`. Ensure that the value used during SSR and hydration is the same.

### `useUpdatingRef(value)`

Custom hook which creates a ref to the `value` which is kept up-to-date after each render in `useLayoutEffect`.
Expand Down
75 changes: 75 additions & 0 deletions packages/kerosene-ui/src/hooks/useTimeZone.spec.tsx
@@ -0,0 +1,75 @@
import { act, renderHook } from "@testing-library/react";
import { identity } from "lodash";
import * as React from "react";
import useTimeZone, { TimeZoneProvider } from "./useTimeZone";

describe("useTimeZone", () => {
let onRender: jest.Mock<string, [string]>;
let resolvedOptions: jest.SpiedFunction<
Intl.DateTimeFormat["resolvedOptions"]
>;
beforeEach(() => {
window.ontimezonechange = null;
onRender = jest.fn().mockImplementation(identity);
resolvedOptions = jest.spyOn(
Intl.DateTimeFormat.prototype,
"resolvedOptions",
);
resolvedOptions.mockReturnValue({
timeZone: "Australia/Sydney",
} as Intl.ResolvedDateTimeFormatOptions);
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should return Australia/Sydney", () => {
const { result } = renderHook(() => useTimeZone());
expect(result.current).toBe("Australia/Sydney");
});

it("should return Etc/UTC during hydration", () => {
renderHook(() => onRender(useTimeZone()), { hydrate: true });
expect(onRender).toHaveBeenNthCalledWith(1, "Etc/UTC");
expect(onRender).toHaveBeenLastCalledWith("Australia/Sydney");
});

it("should return ssrTimeZone from context during hydration", () => {
renderHook(() => onRender(useTimeZone()), {
hydrate: true,
wrapper({ children }) {
return (
<TimeZoneProvider ssrTimeZone="Australia/Brisbane">
{children}
</TimeZoneProvider>
);
},
});
expect(onRender).toHaveBeenNthCalledWith(1, "Australia/Brisbane");
expect(onRender).toHaveBeenLastCalledWith("Australia/Sydney");
});

it("should response to timezonechange events", () => {
const { result } = renderHook(() => useTimeZone());
resolvedOptions.mockReturnValue({
timeZone: "Australia/Brisbane",
} as Intl.ResolvedDateTimeFormatOptions);
act(() => {
window.dispatchEvent(new Event("timezonechange"));
});
expect(result.current).toBe("Australia/Brisbane");
});

it("should response to visibilitychange events when ontimezonechange is not supported", () => {
delete window.ontimezonechange;
const { result } = renderHook(() => useTimeZone());
resolvedOptions.mockReturnValue({
timeZone: "Australia/Brisbane",
} as Intl.ResolvedDateTimeFormatOptions);
act(() => {
document.dispatchEvent(new Event("visibilitychange"));
});
expect(result.current).toBe("Australia/Brisbane");
});
});
69 changes: 69 additions & 0 deletions packages/kerosene-ui/src/hooks/useTimeZone.tsx
@@ -0,0 +1,69 @@
import * as React from "react";
import { useSyncExternalStore } from "use-sync-external-store/shim";

declare global {
interface WindowEventHandlersEventMap {
timezonechange: Event;
}

interface WindowEventHandlers {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ontimezonechange?: ((this: Window, ev: Event) => any) | null;
}
}

function subscribe(callback: () => void): () => void {
// @see https://github.com/whatwg/html/pull/3047
if ("ontimezonechange" in window) {
window.addEventListener("timezonechange", callback);
return () => {
window.removeEventListener("timezonechange", callback);
};
}

// If the "timezonechange" event is not supported, use "visibilitychange" as a proxy
document.addEventListener("visibilitychange", callback);
return () => document.removeEventListener("visibilitychange", callback);
}

function getSnapshot() {
return new Intl.DateTimeFormat().resolvedOptions().timeZone;
}

const SSRTimeZoneContext = React.createContext("Etc/UTC");

/**
* Custom hook which returns the current `timeZone`.
*
* Defaults to `"Etc/UTC"` during SSR and hydration, but this may be overriden with a provider
* `<TimeZoneProvider ssrTimeZone={timeZone}>`. Ensure that the value used during SSR and hydration is the same.
* @returns IANA tz database identifier
* @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
*/
export default function useTimeZone() {
const ssrTimeZone = React.useContext(SSRTimeZoneContext);
const getServerSnapshot = React.useCallback(() => ssrTimeZone, [ssrTimeZone]);

return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

export interface TimeZoneProviderProps {
children?: React.ReactNode;
/** IANA tz database identifier */
ssrTimeZone: string;
}

/**
* Context Provider for the `useTimeZone` hook. May be used to override the default `"Etc/UTC"` `timeZone` value during
* SSR and hydration.
* @param props.children
* @param props.ssrTimeZone IANA tz database identifier
*/
export const TimeZoneProvider = ({
children,
ssrTimeZone,
}: TimeZoneProviderProps) => (
<SSRTimeZoneContext.Provider value={ssrTimeZone}>
{children}
</SSRTimeZoneContext.Provider>
);

0 comments on commit 2959ec6

Please sign in to comment.