Skip to content

Commit 4bf698b

Browse files
committed
fix: differend mode to hide element with Show component
1 parent 41b085d commit 4bf698b

3 files changed

Lines changed: 166 additions & 0 deletions

File tree

src/components/Show.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Children, cloneElement, CSSProperties, isValidElement, ReactElement, ReactNode } from "react";
2+
import { ShowProps } from "../models";
3+
import { useId } from "../hooks";
4+
5+
function isSingleNativeElement(children: ReactNode): ReactElement | null {
6+
const arr = Children.toArray(children);
7+
if (arr.length > 1) {
8+
throw Error("Show component must have exactly one child");
9+
}
10+
if (arr.length === 1 && isValidElement(arr[0]) && typeof arr[0].type === "string") {
11+
return arr[0];
12+
}
13+
return null;
14+
}
15+
16+
/**
17+
* **`Show`**: Generic component used to conditionally render part of the view.
18+
*
19+
* Renders `children` when `when` is truthy. When falsy, the behaviour depends
20+
* on the `mode` prop:
21+
*
22+
* ---
23+
*
24+
* ### `mode="unmount"` _(default)_
25+
* Children are fully removed from the DOM. `fallback` is rendered instead
26+
* (if provided), otherwise `null`. State inside the subtree is lost on hide
27+
* and reset on re-show.
28+
*
29+
* ---
30+
*
31+
* ### `mode="hidden"`
32+
* Children always remain in the DOM, preserving their state and avoiding
33+
* remount costs. Hiding is achieved via `display: none`:
34+
*
35+
* - **Native DOM children** (e.g. `<div>`, `<span>`): the `hidden` HTML
36+
* attribute is injected directly onto the root element via `cloneElement`.
37+
* No wrapper element is introduced. `fallback` is rendered alongside the
38+
* hidden children regardless of `when`.
39+
* - **Custom component children**: a `<div style={{ display: "none" }}>` wrapper
40+
* is introduced only when `when` is falsy. When `when` is truthy the wrapper
41+
* is absent and children are rendered unwrapped, so the layout is unaffected
42+
* in the visible state. `fallback` is rendered alongside when `when` is falsy.
43+
*
44+
* ---
45+
*
46+
* ### `mode="visibility"`
47+
* Children always remain in the DOM. Hiding preserves the layout space
48+
* occupied by the element:
49+
*
50+
* - **Native DOM children**: `visibility: hidden` is merged into the root
51+
* element's inline style via `cloneElement`. No wrapper is introduced.
52+
* `fallback` is rendered alongside when `when` is falsy.
53+
* - **Custom component children**: a wrapper `<div>` with `display: contents`
54+
* (visible) or `display: none` (hidden) is always present in the DOM to
55+
* preserve component state across visibility changes.
56+
* ⚠️ **Layout limitation**: `display: contents` is incompatible with
57+
* `visibility: hidden`, so layout space is **not** preserved for custom
58+
* components — the element is fully removed from flow when hidden, identical
59+
* to `mode="hidden"`. To get true `visibility: hidden` behaviour, wrap the
60+
* custom component in a native element (e.g. `<div><MyComponent /></div>`)
61+
* so that `Show` can apply `visibility: hidden` via `cloneElement` directly
62+
* on the native wrapper.
63+
*
64+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/Show)
65+
* @template T - The type of the `when` value. When truthy, `children` are rendered; otherwise `fallback` is shown.
66+
* @param {ShowProps<T>} props - {@link ShowProps}
67+
* @returns {JSX.Element|null} element - the element rendered or null.
68+
*/
69+
function Show<T>({ when, fallback, children, mode = "unmount" }: ShowProps<T>) {
70+
const id = useId().replace(/:/g, "");
71+
void id;
72+
73+
if (mode === "hidden") {
74+
const native = isSingleNativeElement(children);
75+
if (native) {
76+
return (
77+
<>
78+
{cloneElement(native, { hidden: !when || undefined } as any)}
79+
{!when && (fallback ?? null)}
80+
</>
81+
);
82+
}
83+
return <>
84+
<div style={{ display: when ? "contents" : "none" }}>{children}</div>
85+
{!when && (fallback ?? null)}
86+
</>
87+
}
88+
89+
if (mode === "visibility") {
90+
const native = isSingleNativeElement(children);
91+
if (native) {
92+
const existingStyle: CSSProperties = (native.props as any).style ?? {};
93+
return (
94+
<>
95+
{cloneElement(native, {
96+
style: { ...existingStyle, visibility: when ? "visible" : "hidden" },
97+
} as any)}
98+
{!when && (fallback ?? null)}
99+
</>
100+
);
101+
}
102+
return <>
103+
<div style={{ display: when ? "contents" : "none" }}>{children}</div>
104+
{!when && (fallback ?? null)}
105+
</>
106+
}
107+
108+
if (!when) {
109+
return fallback ? <>{fallback}</> : null;
110+
}
111+
return <>{children}</>;
112+
}
113+
114+
export { Show };

src/components/ShowMemoized.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { memo } from "react";
2+
import { Show } from "./Show";
3+
4+
//#IGNORE
5+
6+
/**
7+
* **`ShowMemoized`**: Memoized version of _Show_ component.
8+
* {@link Show} is wrapped with `React.memo`, preventing re-renders when his props have
9+
* not changed. Prefer this over {@link Show} in performance-sensitive
10+
* trees where the parent re-renders frequently.
11+
*
12+
* @see [📖 Documentation](https://react-tools.ndria.dev/components/ShowMemoized)
13+
*/
14+
export const ShowMemoized = memo(Show);

src/models/Show.model.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { PropsWithChildren, ReactNode } from "react";
2+
3+
/**
4+
* Props accepted by the [Show](https://react-tools.ndria.dev/components/Show) component.
5+
*
6+
* @template T - The type of the `when` value. When truthy, `children` are rendered; otherwise `fallback` is shown.
7+
*/
8+
export interface ShowProps<T> extends PropsWithChildren {
9+
/**
10+
* The condition that controls rendering. When truthy, `children` are
11+
* displayed; when falsy (`false`, `null`, or `undefined`), `fallback`
12+
* is rendered instead (or nothing if `fallback` is omitted).
13+
*/
14+
when: T | boolean | undefined | null;
15+
16+
/**
17+
* Optional content rendered when `when` is falsy. Accepts any valid
18+
* React node. When omitted, nothing is rendered in the falsy case.
19+
*/
20+
fallback?: ReactNode;
21+
22+
/**
23+
* Controls how the component behaves when `when` is falsy:
24+
*
25+
* - `"unmount"` _(default)_ — children are fully removed from the DOM.
26+
* The `fallback` prop is rendered instead (if provided).
27+
* - `"hidden"` — children remain in the DOM. When `when` is falsy, the
28+
* `hidden` HTML attribute is applied directly to the children's root
29+
* element via `cloneElement`. When `when` is truthy the attribute is removed.
30+
* - `"visibility"` — children remain in the DOM. When `when` is falsy,
31+
* `display: none !important` CSS rule is applied directly to the
32+
* children'root element via `cloneElement`. When `when` is truthy
33+
* the rule is removed.
34+
*
35+
* @default "unmount"
36+
*/
37+
mode?: "unmount" | "hidden" | "visibility";
38+
}

0 commit comments

Comments
 (0)