Skip to content

Commit

Permalink
fix: long title on small screen occupies too much space
Browse files Browse the repository at this point in the history
  • Loading branch information
czabaj committed Jan 2, 2025
1 parent b8cd6ce commit 172bc2b
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 7 deletions.
11 changes: 11 additions & 0 deletions src/components/Header/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
grid-template-columns: auto 1fr auto;
margin-block-start: 0.5rem;
padding: var(--border-size-2);

&[data-layout="xl"] {
& h2 {
white-space: nowrap; /* force overflow when title large to trigger layout change */
}
}
&[data-layout="sm"] {
& h2 {
font-size: 1em;
}
}
}
.buttonLeft {
border-inline-end: var(--border-size-2) solid var(--surface-1);
Expand Down
19 changes: 13 additions & 6 deletions src/components/Header/Header.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ type classesType = {buttonLeft: string, buttonRight: string, root: string}

@react.component
let make = (~buttonLeftSlot, ~buttonRightSlot, ~className=?, ~headingSlot, ~subheadingSlot) => {
<header className={`${classes.root} ${className->Option.getOr("")}`}>
<h2> {headingSlot} </h2>
<p> {subheadingSlot} </p>
{buttonLeftSlot}
{buttonRightSlot}
</header>
let headerRef = React.useRef(Nullable.null)
let layout = Hooks.useIsHorizontallyOverflowing(headerRef.current, [`xl`, `sm`])
React.cloneElement(
<header
className={`${classes.root} ${className->Option.getOr("")}`}
ref={headerRef->ReactDOM.Ref.domRef}>
<h2> {headingSlot} </h2>
<p> {subheadingSlot} </p>
{buttonLeftSlot}
{buttonRightSlot}
</header>,
{"data-layout": layout},
)
}
109 changes: 108 additions & 1 deletion src/utils/Hooks.res
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,111 @@ let usePromise = (fn: unit => promise<'data>) => {
->ignore
}
(result, run)
}
}

type overflowingState<'a> = {
currentLayoutIndex: int,
layouts: array<'a>,
// thresholds is used as a mutable array, never coppied
thresholds: array<int>,
}

type overfowingAction = Resized({availableWidth: int, contentWidth: int})

let overflowingReducer: (overflowingState<'a>, overfowingAction) => overflowingState<'a> = (
state,
action,
) => {
let newState = ref(state)
switch action {
| Resized({availableWidth, contentWidth}) => {
let {currentLayoutIndex, thresholds} = state
let overflowing = contentWidth > availableWidth
if !overflowing {
let canGrow = currentLayoutIndex !== 0
if canGrow {
let widerLayoutThreshold = thresholds->Array.getUnsafe(currentLayoutIndex - 1)
if availableWidth > widerLayoutThreshold {
newState := {
...state,
currentLayoutIndex: state.currentLayoutIndex - 1,
}
}
}
} else {
// update the threshold if the content width is different
let currentLayoutThreshold = thresholds->Array.getUnsafe(currentLayoutIndex)
if contentWidth !== currentLayoutThreshold {
thresholds->Array.splice(~start=currentLayoutIndex, ~remove=1, ~insert=[contentWidth])
if %raw(`import.meta.env.DEV`) && currentLayoutIndex !== 0 {
let widerLayoutThreshold = thresholds->Array.getUnsafe(currentLayoutIndex - 1)
if contentWidth > widerLayoutThreshold {
LogUtils.captureException(
TypeUtils.any(
`The threshold for layout "${state.layouts->Array.getUnsafe(
currentLayoutIndex,
)}" (${contentWidth->Int.toString}px) is wider than threshold for layout "${state.layouts->Array.getUnsafe(
currentLayoutIndex - 1,
)}" (${widerLayoutThreshold->Int.toString}px)
The layouts should be sorted from widest to narrowest (e.g. ["XL", "MD", "SM"]). This likely means an error in the rendering logic.`,
),
)
}
}
}
let canShrink = currentLayoutIndex !== state.layouts->Array.length - 1
if canShrink {
newState := {
...state,
currentLayoutIndex: currentLayoutIndex + 1,
}
}
}
}
}
newState.contents
}

/**
* This hooks accepts an array of strings like `["XL", "MD", "SM"]` which must be sorted from widest to narrowest.
* It returns the first item from the layuouts as long as the content does not overflow the parent, when it overflows,
* it returns the next layout and it expects that the content will be adapted for narrower screen. It continues in
* measurement and returns narrower layout as long as there are any available. It remembers the screen threshold where
* it switched and if the screen grows larger again, it switches back to the wider layout.
*
* @example
*
* let layout = useIsHorizontallyOverflowing(Some(element), [#xl, #md, #sm])
*/
let useIsHorizontallyOverflowing = (element: Nullable.t<Element.t>, layouts: array<'a>) => {
let (overflowState, sendOverflowing) = React.useReducer(
overflowingReducer,
{
currentLayoutIndex: 0,
layouts,
thresholds: [],
},
)
React.useEffect1(() => {
switch element {
| Nullable.Value(el) =>
let testOverflowing = _ => {
let availableWidth = el->Element.clientWidth
let contentWidth = el->Element.scrollWidth
Js.log({"availableWidth": availableWidth, "contentWidth": contentWidth})
sendOverflowing(Resized({availableWidth, contentWidth}))
}
testOverflowing()
let resizeObserver = Webapi.ResizeObserver.make(testOverflowing)
resizeObserver->Webapi.ResizeObserver.observe(el)
Some(
() => {
resizeObserver->Webapi.ResizeObserver.disconnect
},
)
| _ => None
}
}, [element])
layouts->Array.getUnsafe(overflowState.currentLayoutIndex)
}

0 comments on commit 172bc2b

Please sign in to comment.