Skip to content

Commit c10ff2b

Browse files
fix(example): mobile
1 parent a4d1323 commit c10ff2b

2 files changed

Lines changed: 141 additions & 6 deletions

File tree

example/src/App.css

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
44
}
55

6+
*,
7+
*::before,
8+
*::after {
9+
box-sizing: border-box;
10+
}
11+
612
body {
713
margin: 0;
14+
overflow-x: hidden;
815
}
916

1017
.page {
@@ -118,12 +125,26 @@ h1 {
118125
color: #64748b;
119126
}
120127

128+
.mobile-width-control {
129+
display: grid;
130+
gap: 8px;
131+
color: #475569;
132+
font-size: 0.95rem;
133+
font-weight: 600;
134+
}
135+
136+
.mobile-width-control input {
137+
width: 100%;
138+
}
139+
121140
.example-frame-shell {
122141
position: relative;
142+
width: 100%;
123143
min-width: 0;
124144
}
125145

126146
.example-frame {
147+
width: 100%;
127148
min-width: 180px;
128149
max-width: 100%;
129150
padding: 14px;
@@ -182,7 +203,7 @@ h1 {
182203
.popover {
183204
position: absolute;
184205
z-index: 10;
185-
max-width: min(320px, calc(100% - 16px));
206+
max-width: min(320px, calc(100vw - 16px));
186207
padding: 12px;
187208
border: 1px solid #e5e7eb;
188209
border-radius: 14px;
@@ -216,4 +237,8 @@ h1 {
216237
h1 {
217238
font-size: 2.7rem;
218239
}
240+
241+
.example-frame {
242+
resize: none;
243+
}
219244
}

example/src/App.tsx

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type PopoverState = {
1414
hiddenItems: Item[];
1515
left: number;
1616
top: number;
17+
anchorLeft: number;
1718
};
1819

1920
const items: Item[] = [
@@ -28,16 +29,38 @@ const items: Item[] = [
2829
const githubUrl =
2930
"https://github.com/SincerelyFaust/react-fit-list?tab=readme-ov-file";
3031
const npmUrl = "https://www.npmjs.com/package/react-fit-list";
32+
const DESKTOP_FRAME_WIDTH = 360;
33+
const MOBILE_FRAME_MIN_WIDTH = 220;
3134

3235
function Tag({ children }: { children: React.ReactNode }) {
3336
return <span className="tag">{children}</span>;
3437
}
3538

3639
function App() {
3740
const [frameWidth, setFrameWidth] = useState(320);
41+
const [mobileFrameWidth, setMobileFrameWidth] = useState(DESKTOP_FRAME_WIDTH);
42+
const [isMobileViewport, setIsMobileViewport] = useState(false);
3843
const [popover, setPopover] = useState<PopoverState | null>(null);
3944
const frameShellRef = useRef<HTMLDivElement | null>(null);
4045
const frameRef = useRef<HTMLDivElement | null>(null);
46+
const popoverRef = useRef<HTMLDivElement | null>(null);
47+
48+
useEffect(() => {
49+
if (typeof window === "undefined") return;
50+
51+
const mediaQuery = window.matchMedia("(max-width: 640px)");
52+
const syncViewportMode = () => setIsMobileViewport(mediaQuery.matches);
53+
54+
syncViewportMode();
55+
56+
if (typeof mediaQuery.addEventListener === "function") {
57+
mediaQuery.addEventListener("change", syncViewportMode);
58+
return () => mediaQuery.removeEventListener("change", syncViewportMode);
59+
}
60+
61+
mediaQuery.addListener(syncViewportMode);
62+
return () => mediaQuery.removeListener(syncViewportMode);
63+
}, []);
4164

4265
useEffect(() => {
4366
const frame = frameRef.current;
@@ -55,7 +78,23 @@ function App() {
5578
return () => {
5679
observer.disconnect();
5780
};
58-
}, []);
81+
}, [isMobileViewport, mobileFrameWidth]);
82+
83+
useEffect(() => {
84+
if (typeof window === "undefined") return;
85+
86+
const syncMobileWidth = () => {
87+
const nextWidth = Math.min(DESKTOP_FRAME_WIDTH, window.innerWidth - 32);
88+
setMobileFrameWidth((current) => {
89+
if (!isMobileViewport) return current;
90+
return Math.max(MOBILE_FRAME_MIN_WIDTH, nextWidth);
91+
});
92+
};
93+
94+
syncMobileWidth();
95+
window.addEventListener("resize", syncMobileWidth);
96+
return () => window.removeEventListener("resize", syncMobileWidth);
97+
}, [isMobileViewport]);
5998

6099
useEffect(() => {
61100
if (!popover) return;
@@ -80,6 +119,43 @@ function App() {
80119
};
81120
}, [popover]);
82121

122+
useEffect(() => {
123+
if (!popover) return;
124+
125+
const updatePopoverPosition = () => {
126+
const frameShell = frameShellRef.current;
127+
const node = popoverRef.current;
128+
if (!frameShell || !node) return;
129+
130+
const shellRect = frameShell.getBoundingClientRect();
131+
const popoverWidth = node.offsetWidth;
132+
const viewportPadding = 8;
133+
const minLeft = Math.max(viewportPadding - shellRect.left, 8);
134+
const maxLeft = Math.max(
135+
minLeft,
136+
window.innerWidth - viewportPadding - shellRect.left - popoverWidth
137+
);
138+
const nextLeft = Math.min(Math.max(popover.anchorLeft, minLeft), maxLeft);
139+
140+
setPopover((current) => {
141+
if (!current || current.left === nextLeft) return current;
142+
return {
143+
...current,
144+
left: nextLeft,
145+
};
146+
});
147+
};
148+
149+
updatePopoverPosition();
150+
window.addEventListener("resize", updatePopoverPosition);
151+
window.addEventListener("scroll", updatePopoverPosition, true);
152+
153+
return () => {
154+
window.removeEventListener("resize", updatePopoverPosition);
155+
window.removeEventListener("scroll", updatePopoverPosition, true);
156+
};
157+
}, [popover]);
158+
83159
const openOverflowPopover = (
84160
args: FitListOverflowRenderArgs<Item>,
85161
event: React.MouseEvent<HTMLElement>
@@ -90,6 +166,7 @@ function App() {
90166

91167
const triggerRect = event.currentTarget.getBoundingClientRect();
92168
const frameRect = frameShell.getBoundingClientRect();
169+
const anchorLeft = Math.max(8, triggerRect.right - frameRect.left - 46);
93170

94171
setPopover((current) => {
95172
const isSame =
@@ -105,12 +182,21 @@ function App() {
105182

106183
return {
107184
hiddenItems: args.hiddenItems,
108-
left: Math.max(8, triggerRect.right - frameRect.left - 46),
185+
left: anchorLeft,
186+
anchorLeft,
109187
top: triggerRect.bottom - frameRect.top + 10,
110188
};
111189
});
112190
};
113191

192+
const mobileSliderMax =
193+
typeof window === "undefined"
194+
? DESKTOP_FRAME_WIDTH
195+
: Math.max(
196+
MOBILE_FRAME_MIN_WIDTH,
197+
Math.min(DESKTOP_FRAME_WIDTH, window.innerWidth - 32)
198+
);
199+
114200
return (
115201
<main className="page">
116202
<div className="content">
@@ -154,16 +240,39 @@ function App() {
154240
<span className="width-value">{frameWidth}px</span>
155241
</div>
156242
<p className="panel-description">
157-
Drag the resize handle to test how the list fits and when the
158-
overflow button appears.
243+
{isMobileViewport
244+
? "Use the slider to preview how the list behaves at smaller widths."
245+
: "Drag the resize handle to test how the list fits and when the overflow button appears."}
159246
</p>
160247
</div>
161248

249+
{isMobileViewport && (
250+
<label className="mobile-width-control">
251+
<span>Preview width</span>
252+
<input
253+
type="range"
254+
min={MOBILE_FRAME_MIN_WIDTH}
255+
max={mobileSliderMax}
256+
step={1}
257+
value={Math.min(mobileFrameWidth, mobileSliderMax)}
258+
onChange={(event) => {
259+
setMobileFrameWidth(Number(event.target.value));
260+
setPopover(null);
261+
}}
262+
aria-label="Preview width"
263+
/>
264+
</label>
265+
)}
266+
162267
<div ref={frameShellRef} className="example-frame-shell">
163268
<div
164269
ref={frameRef}
165270
className="example-frame"
166-
style={{ width: "min(100%, 360px)" }}
271+
style={{
272+
width: isMobileViewport
273+
? `min(100%, ${Math.min(mobileFrameWidth, mobileSliderMax)}px)`
274+
: "min(100%, 360px)",
275+
}}
167276
>
168277
<div className="frame-toolbar" aria-hidden="true">
169278
<span />
@@ -185,6 +294,7 @@ function App() {
185294

186295
{popover && (
187296
<div
297+
ref={popoverRef}
188298
className="popover"
189299
style={{ left: `${popover.left}px`, top: `${popover.top}px` }}
190300
role="dialog"

0 commit comments

Comments
 (0)