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 } ;
0 commit comments