diff --git a/src/components/Header/Header.module.css b/src/components/Header/Header.module.css index 7f92277..e9b5c82 100644 --- a/src/components/Header/Header.module.css +++ b/src/components/Header/Header.module.css @@ -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); diff --git a/src/components/Header/Header.res b/src/components/Header/Header.res index a944ade..3756f35 100644 --- a/src/components/Header/Header.res +++ b/src/components/Header/Header.res @@ -4,10 +4,17 @@ type classesType = {buttonLeft: string, buttonRight: string, root: string} @react.component let make = (~buttonLeftSlot, ~buttonRightSlot, ~className=?, ~headingSlot, ~subheadingSlot) => { -
Option.getOr("")}`}> -

{headingSlot}

-

{subheadingSlot}

- {buttonLeftSlot} - {buttonRightSlot} -
+ let headerRef = React.useRef(Nullable.null) + let layout = Hooks.useIsHorizontallyOverflowing(headerRef.current, [`xl`, `sm`]) + React.cloneElement( +
Option.getOr("")}`} + ref={headerRef->ReactDOM.Ref.domRef}> +

{headingSlot}

+

{subheadingSlot}

+ {buttonLeftSlot} + {buttonRightSlot} +
, + {"data-layout": layout}, + ) } diff --git a/src/utils/Hooks.res b/src/utils/Hooks.res index 95abd46..ce23652 100644 --- a/src/utils/Hooks.res +++ b/src/utils/Hooks.res @@ -20,4 +20,111 @@ let usePromise = (fn: unit => promise<'data>) => { ->ignore } (result, run) -} \ No newline at end of file +} + +type overflowingState<'a> = { + currentLayoutIndex: int, + layouts: array<'a>, + // thresholds is used as a mutable array, never coppied + thresholds: array, +} + +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, 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) +}