Skip to content

Commit

Permalink
fix: added stories
Browse files Browse the repository at this point in the history
  • Loading branch information
nahoc committed Sep 23, 2024
1 parent 3516377 commit ca374da
Show file tree
Hide file tree
Showing 28 changed files with 794 additions and 360 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ https://www.npmjs.com/package/@risc0/ui

| Statements | Branches | Functions | Lines |
| --------------------------- | ----------------------- | ------------------------- | ----------------- |
| ![Statements](https://img.shields.io/badge/statements-38.65%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-74.68%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-61.9%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-38.65%25-red.svg?style=flat) |
| ![Statements](https://img.shields.io/badge/statements-40.8%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-78.49%25-red.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-69.81%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-40.8%25-red.svg?style=flat) |
4 changes: 2 additions & 2 deletions config/biome.base.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"useSingleCaseStatement": "error",
"useNumberNamespace": "error",
"useFilenamingConvention": {
"level": "warn",
"level": "info",
"options": {
"strictCase": true,
"requireAscii": true,
Expand All @@ -66,7 +66,7 @@
},
"nursery": {
"useSortedClasses": {
"level": "warn",
"level": "info",
"options": {
"attributes": ["classList"],
"functions": ["clsx", "cva", "tw", "cn"]
Expand Down
35 changes: 19 additions & 16 deletions hooks/use-local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"use client";

import { isEqual, isFunction } from "radash";
import { type Dispatch, type SetStateAction, useEffect, useRef, useState } from "react";
import { parseJson } from "../utils/parse-json";
import { useEventListener } from "./use-event-listener";

const isNil = (val: unknown) => val == null;
function isNil(val: unknown) {
return val == null;
}

export type SetValue<T> = Dispatch<SetStateAction<T>>;

Expand All @@ -14,7 +18,7 @@ export type SetValue<T> = Dispatch<SetStateAction<T>>;
* @param key the key to use to store
* @returns the value requested
*/
const readValueFromStorage = <T>(key: string) => {
function readValueFromStorage<T>(key: string) {
try {
const item = window.localStorage.getItem(key);
const value = item && (parseJson(item) as T);
Expand All @@ -25,20 +29,22 @@ const readValueFromStorage = <T>(key: string) => {

return { error: "unable to read value" };
}
};
}

type StorageError = {
error: string;
};

const isError = (value: StorageError | any): value is StorageError => !!value?.error;
function isError(value: StorageError | any): value is StorageError {
return !!value?.error;
}

export const useLocalStorage = <T>(key: string, initialValue: T): [T, SetValue<T>] => {
export function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
const previousValueRef = useRef(initialValue);

// Get from local storage then
// parse stored json or return initialValue
const readValue = (): T => {
function readValue(): T {
// Prevent build error "window is undefined" but keep keep working
if (typeof window === "undefined") {
return initialValue;
Expand All @@ -47,15 +53,14 @@ export const useLocalStorage = <T>(key: string, initialValue: T): [T, SetValue<T
const value = readValueFromStorage<T>(key);

return isError(value) || isNil(value) || value === "" ? initialValue : (value as T);
};
}

// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(readValue);

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: SetValue<T> = (value) => {
// Return a wrapped version of useState's setter function that persists the new value to localStorage.
function setValue(value: SetStateAction<T>): void {
// Prevent build error "window is undefined" but keeps working
if (typeof window === "undefined") {
console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`);
Expand All @@ -76,26 +81,24 @@ export const useLocalStorage = <T>(key: string, initialValue: T): [T, SetValue<T
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
}

// biome-ignore lint/correctness/useExhaustiveDependencies: run only once
useEffect(() => {
const newValue = readValue();
setStoredValue(newValue);
previousValueRef.current = newValue;
// run only once
}, []);

const handleStorageChange = () => {
function handleStorageChange() {
const newValue = readValue();

if (isEqual(newValue, previousValueRef.current)) {
return;
}
setStoredValue(newValue);
previousValueRef.current = newValue;
// run only once
};
}

// this only works for other documents, not the current one
useEventListener("storage", handleStorageChange);
Expand All @@ -105,4 +108,4 @@ export const useLocalStorage = <T>(key: string, initialValue: T): [T, SetValue<T
useEventListener("local-storage", handleStorageChange);

return [storedValue, setValue];
};
}
92 changes: 92 additions & 0 deletions hooks/use-media-query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { act, renderHook } from "@testing-library/react";
import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useMediaQuery } from "./use-media-query";

describe("useMediaQuery", () => {
let matchMediaMock: Mock;

beforeEach(() => {
matchMediaMock = vi.fn();
window.matchMedia = matchMediaMock;
});

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

function createMatchMedia(matches: boolean) {
return () => ({
matches,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
});
}

it("should return initial value based on media query", () => {
matchMediaMock.mockImplementation(createMatchMedia(true));

const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));

expect(result.current).toBe(true);
});

it("should update value when media query changes", () => {
const listeners: ((event: MediaQueryListEvent) => void)[] = [];
matchMediaMock.mockImplementation(() => ({
matches: false,
addEventListener: (_, listener) => listeners.push(listener),
removeEventListener: (_, listener) => {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
},
}));

const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));

expect(result.current).toBe(false);
act(() => {
for (const listener of listeners) {
listener({ matches: true } as MediaQueryListEvent);
}
});

expect(result.current).toBe(true);
});

it("should remove event listener on unmount", () => {
const removeEventListenerMock = vi.fn();
matchMediaMock.mockImplementation(() => ({
matches: true,
addEventListener: vi.fn(),
removeEventListener: removeEventListenerMock,
}));

const { unmount } = renderHook(() => useMediaQuery("(min-width: 768px)"));

unmount();

expect(removeEventListenerMock).toHaveBeenCalledWith("change", expect.any(Function));
});

it("should update when query changes", () => {
let currentQuery = "(min-width: 768px)";
matchMediaMock.mockImplementation(() => ({
matches: currentQuery === "(min-width: 768px)",
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));

const { result, rerender } = renderHook(({ query }) => useMediaQuery(query), {
initialProps: { query: currentQuery },
});

expect(result.current).toBe(true);

currentQuery = "(max-width: 480px)";
rerender({ query: currentQuery });

expect(result.current).toBe(false);
});
});
2 changes: 2 additions & 0 deletions hooks/use-media-query.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect, useState } from "react";

export function useMediaQuery(query: string) {
Expand Down
2 changes: 2 additions & 0 deletions hooks/use-mounted.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect, useState } from "react";

export function useMounted() {
Expand Down
62 changes: 0 additions & 62 deletions hooks/use-step.ts

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@risc0/ui",
"version": "0.0.171",
"version": "0.0.172",
"private": false,
"sideEffects": false,
"type": "module",
Expand Down
43 changes: 28 additions & 15 deletions stories/alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
/* c8 ignore start */
import { Alert, AlertDescription, AlertTitle } from "alert";
import { RocketIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "../alert";

export const Default = () => (
<Alert>
<RocketIcon className="size-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>You can add components to your app using the cli.</AlertDescription>
</Alert>
);
export function Default() {
return (
<Alert>
<RocketIcon className="size-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>You can add components to your app using the cli.</AlertDescription>
</Alert>
);
}

export const Destructive = () => (
<Alert variant="destructive">
<RocketIcon className="size-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>You can add components to your app using the cli.</AlertDescription>
</Alert>
);
export function Destructive() {
return (
<Alert variant="destructive">
<RocketIcon className="size-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Your session has expired. Please log in again.</AlertDescription>
</Alert>
);
}

export function WithoutIcon() {
return (
<Alert>
<AlertTitle>Note</AlertTitle>
<AlertDescription>This is a simple alert without an icon.</AlertDescription>
</Alert>
);
}
Loading

0 comments on commit ca374da

Please sign in to comment.