Skip to content

Commit

Permalink
fix: <MenuModal> column layout
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout committed Oct 30, 2024
1 parent 8735857 commit 0940201
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 160 deletions.
2 changes: 2 additions & 0 deletions src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { Layout }
export { containerQueryMobile }
export { navWidthMin }
export { navWidthMax }

import React from 'react'
import { NavigationContent } from './navigation/Navigation'
Expand Down
6 changes: 1 addition & 5 deletions src/config/resolveHeadingsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,12 @@ function resolveHeadingsData(pageContext: PageContextOriginal) {
// TODO/refactor: remove navItems
let navItems: NavItem[]
let navItemsAll: NavItemAll[]
let columnLayouts: number[][]
{
const navItemsPageSections = pageSectionsResolved
.filter((pageSection) => pageSection.pageSectionLevel === 2)
.map(pageSectionToNavItem)
navItemsAll = headingsResolved.map(headingToNavItem)
const res = determineColumnLayoutEntries(navItemsAll)
columnLayouts = res.columnLayouts
determineColumnLayoutEntries(navItemsAll)
if (isDetachedPage) {
navItems = [headingToNavItem(activeHeading), ...navItemsPageSections]
} else {
Expand All @@ -85,8 +83,6 @@ function resolveHeadingsData(pageContext: PageContextOriginal) {
pageTitle,
documentTitle,
// TODO: don't pass to client-side
columnLayouts,
// TODO: don't pass to client-side
activeCategory,
}
return pageContextAddendum
Expand Down
116 changes: 72 additions & 44 deletions src/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { usePageContext } from '../renderer/usePageContext'
import '@docsearch/css'
import '../global.d.ts'
import { getViewportWidth } from '../utils/getViewportWidth'
import { navWidthMax, navWidthMin } from '../Layout'

type NavItem = {
level: number
Expand All @@ -22,7 +23,8 @@ type NavItem = {
menuModalFullWidth?: true
}
type NavItemAll = NavItem & {
isColumnLayoutElement?: true
// TODO/refactor: rename to isColumnLayoutEntry
isColumnLayoutElement?: ColumnMap
}
function NavigationContent(props: {
navItems: NavItem[]
Expand Down Expand Up @@ -50,49 +52,51 @@ function NavigationContent(props: {
}

function NavigationColumnLayout(props: { navItemsWithComputed: NavItemComputed[] }) {
const navItemsColumnLayout = groupByColumnLayout(props.navItemsWithComputed)
const paddingBottom = 40
let [viewportWidth, setViewportWidth] = useState<number | undefined>()
const updateviewportwidth = () => setViewportWidth(getViewportWidth())
useEffect(() => {
updateviewportwidth()
window.addEventListener('resize', updateviewportwidth, { passive: true })
})

const navItemsByColumnLayouts = getNavItemsByColumnLayouts(props.navItemsWithComputed, viewportWidth)

return (
<>
{navItemsColumnLayout.map(({ navItemsColumnEntries, isFullWidth }, i) => (
{navItemsByColumnLayouts.map(({ columns, isFullWidth }, i) => (
<div
key={i}
style={{
display: 'flex',
justifyContent: 'center',
width: columns.length * (navWidthMax + 20),
justifyContent: 'space-between',
maxWidth: '100%',
margin: 'auto',
marginBottom: 40,
}}
>
<div
className={`column-layout-${i}`}
style={{
flexGrow: 1,
columnGap: 20,
paddingBottom: isFullWidth ? paddingBottom : undefined,
}}
>
{navItemsColumnEntries.map((navItemColumnEntry, j) => (
<div
key={j}
className="column-layout-entry"
style={{
breakInside: 'avoid',
paddingBottom: !isFullWidth ? paddingBottom : undefined,
paddingTop: isFullWidth ? undefined : 0,
width: '100%',
}}
>
<div>
<NavItemComponent navItem={navItemColumnEntry} />
{navItemColumnEntry.navItemChilds.map((navItem, k) => (
<NavItemComponent navItem={navItem} key={k} />
{columns.map((columnEntry, j) => (
<div
key={j}
style={{
flexGrow: 1,
maxWidth: navWidthMax,
display: 'flex',
flexDirection: 'column',
paddingTop: isFullWidth && j !== 0 ? 36 : undefined,
}}
>
{columnEntry.map((navItems, k) => (
<div key={k}>
{navItems.map((navItem, l) => (
<NavItemComponent navItem={navItem} key={l} />
))}
<CategoryBorder navItemLevel1={isFullWidth ? undefined : navItemColumnEntry!} />
<CategoryBorder navItemLevel1={isFullWidth ? undefined : navItems[0]!} />
</div>
</div>
))}
<CategoryBorder navItemLevel1={!isFullWidth ? undefined : navItemsColumnEntries[0]!} />
</div>
))}
</div>
))}
<CategoryBorder navItemLevel1={!isFullWidth ? undefined : columns[0][0][0]!} />
</div>
))}
</>
Expand Down Expand Up @@ -164,33 +168,57 @@ function NavItemComponent({
}
}

type NavItemsColumnEntry = NavItemComputed & { navItemChilds: NavItemComputed[] }
function groupByColumnLayout(navItems: NavItemComputed[]) {
const navItemsColumnLayout: { navItemsColumnEntries: NavItemsColumnEntry[]; isFullWidth: boolean }[] = []
let navItemsColumnEntries: NavItemsColumnEntry[] = []
type NavItemsByColumnLayout = { columns: NavItemComputed[][][]; isFullWidth: boolean }
function getNavItemsByColumnLayouts(navItems: NavItemComputed[], viewportWidth: number = 0): NavItemsByColumnLayout[] {
const navItemsByColumnEntries = getNavItemsByColumnEntries(navItems)
const numberOfColumnsMax = Math.floor(viewportWidth / navWidthMin) || 1
const navItemsByColumnLayouts: NavItemsByColumnLayout[] = navItemsByColumnEntries.map(
({ columnEntries, isFullWidth }) => {
const numberOfColumns = Math.min(numberOfColumnsMax, columnEntries.length)
const columns: NavItemComputed[][][] = []
columnEntries.forEach((columnEntry) => {
const idx = numberOfColumns === 1 ? 0 : columnEntry.columnMap[numberOfColumns]!
assert(idx >= 0)
columns[idx] ??= []
columns[idx].push(columnEntry.navItems)
})
const navItemsByColumnLayout: NavItemsByColumnLayout = { columns, isFullWidth }
return navItemsByColumnLayout
},
)
return navItemsByColumnLayouts
}

type NavItemsByColumnEntries = { columnEntries: ColumnEntry[]; isFullWidth: boolean }[]
type ColumnEntry = { navItems: NavItemComputed[]; columnMap: ColumnMap }
type ColumnMap = Record<number, number>
function getNavItemsByColumnEntries(navItems: NavItemComputed[]): NavItemsByColumnEntries {
const navItemsByColumnEntries: NavItemsByColumnEntries = []
let columnEntries: ColumnEntry[] = []
let columnEntry: ColumnEntry
let isFullWidth: boolean | undefined
navItems.forEach((navItem) => {
if (navItem.level === 1) {
const isFullWidthPrevious = isFullWidth
isFullWidth = !!navItem.menuModalFullWidth
if (isFullWidthPrevious !== undefined && isFullWidthPrevious !== isFullWidth) {
navItemsColumnLayout.push({ navItemsColumnEntries, isFullWidth: isFullWidthPrevious })
navItemsColumnEntries = []
navItemsByColumnEntries.push({ columnEntries, isFullWidth: isFullWidthPrevious })
columnEntries = []
}
}
assert(isFullWidth !== undefined)
if (navItem.isColumnLayoutElement) {
assert(navItem.level === 1 || navItem.level === 4)
const navItemColumnEntry = { ...navItem, navItemChilds: [] }
navItemsColumnEntries.push(navItemColumnEntry)
columnEntry = { navItems: [navItem], columnMap: navItem.isColumnLayoutElement }
columnEntries.push(columnEntry)
} else {
assert(navItem.level !== 1)
navItemsColumnEntries[navItemsColumnEntries.length - 1].navItemChilds.push(navItem)
columnEntry.navItems.push(navItem)
}
})
assert(isFullWidth !== undefined)
navItemsColumnLayout.push({ navItemsColumnEntries, isFullWidth })
return navItemsColumnLayout
navItemsByColumnEntries.push({ columnEntries, isFullWidth })
return navItemsByColumnEntries
}

type NavItemComputed = ReturnType<typeof getNavItemsWithComputed>[number]
Expand Down
125 changes: 18 additions & 107 deletions src/renderer/getStyleColumnLayout.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,14 @@
export { getStyleColumnLayout }
export { determineColumnLayoutEntries }

// A CSS-only solution doesn't seem to exist.
// - The CSS Column Layout (`column-count`) solution down below is hackish and not finished.
// - Maybe it doesn't even work in Safari.
// - Cannot use flexbox.
// - We cannot control wrapping:
// - https://stackoverflow.com/questions/45862033/forcing-a-wrap-on-column-flex-box-layout
// - https://stackoverflow.com/questions/45337454/make-flex-items-wrap-to-create-a-new-column
// - https://stackoverflow.com/questions/27119691/how-to-start-a-new-column-in-flex-column-wrap-layout
// - https://stackoverflow.com/questions/55742578/force-flexbox-to-wrap-after-specific-item-direction-column
// - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Mastering_wrapping_of_flex_items
// - Cannot use grid layout.
// - Cannot use single row, as two elements cannot be put in the same cell:
// - https://stackoverflow.com/questions/45264354/is-it-possible-to-place-more-than-one-element-into-a-css-grid-cell-without-overl/49047281#49047281
// - Trying with mutliple rows seems to be messy(/impractical?) given the table nature of CSS grid.
// - https://stackoverflow.com/questions/45791809/different-height-of-css-grid-cells
// - Couln't make it work with CSS `float`.
// - https://jsfiddle.net/brillout/3hrLk4am/5/
// - Misc:
// - https://stackoverflow.com/questions/9683425/css-column-count-not-respected
// - https://stackoverflow.com/questions/25446921/get-flexbox-column-wrap-to-use-full-width-and-minimize-height
// - https://stackoverflow.com/questions/74873283/how-to-create-a-css-grid-with-3-columns-having-column-flow
// - https://stackoverflow.com/questions/50693793/3-columns-grid-top-to-bottom-using-grid-css
// - https://stackoverflow.com/questions/9119347/html-css-vertical-flow-layout-columnar-style-how-to-implement
// - https://github.com/brillout/docpress/blob/2e41d8b9df098ff8312b02f7e9d41a202548e2b9/src/renderer/getStyleColumnLayout.ts#L4-L26

import { type NavItemAll } from '../navigation/Navigation'
import { css } from '../utils/css'
import { assert, assertUsage, isBrowser } from '../utils/server'
assert(!isBrowser())
const columnWidthMin = 300
const columnWidthMax = 350

type NavItemWithLength = NavItemAll & { numberOfHeadings: number | null }
function determineColumnLayoutEntries(navItems: NavItemAll[]): { columnLayouts: number[][] } {
function determineColumnLayoutEntries(navItems: NavItemAll[]): undefined {
const navItemsWithLength: NavItemWithLength[] = navItems.map((navItem) => ({
...navItem,
numberOfHeadings: navItem.level === 1 || navItem.level === 4 ? 0 : null,
Expand Down Expand Up @@ -61,8 +36,9 @@ function determineColumnLayoutEntries(navItems: NavItemAll[]): { columnLayouts:
}
})

const columnLayouts: number[][] = []
let columns: number[] = []
type ColumnEntry = { navItemLeader: NavItemAll; numberOfEntries: number }
const columnLayouts: ColumnEntry[][] = []
let columnEntries: ColumnEntry[] = []
let isFullWidth: boolean | undefined
navItemsWithLength.forEach((navItem, i) => {
let isFullWidthBegin = false
Expand All @@ -71,8 +47,8 @@ function determineColumnLayoutEntries(navItems: NavItemAll[]): { columnLayouts:
isFullWidth = !!navItem.menuModalFullWidth
if (isFullWidth) isFullWidthBegin = true
if (isFullWidthPrevious !== undefined && isFullWidthPrevious !== isFullWidth) {
columnLayouts.push(columns)
columns = []
columnLayouts.push(columnEntries)
columnEntries = []
}
}
const navItemPrevious = navItemsWithLength[i - 1]
Expand All @@ -97,89 +73,24 @@ function determineColumnLayoutEntries(navItems: NavItemAll[]): { columnLayouts:
assert(navItemNext.numberOfHeadings)
numberOfHeadings = navItemNext.numberOfHeadings
}
columns.push(numberOfHeadings)
navItems[i].isColumnLayoutElement = true
columnEntries.push({ navItemLeader: navItems[i], numberOfEntries: numberOfHeadings })
}
})
columnLayouts.push(columns)
assert(columnEntries!)
columnLayouts.push(columnEntries)

return { columnLayouts }
}

function getStyleColumnLayout(columnLayouts: number[][]): string {
let style =
'\n' +
css`
.column-layout-entry {
break-before: avoid;
}
`
style += '\n'
columnLayouts.forEach((columns, i) => {
for (let numberOfColumns = columns.length; numberOfColumns >= 1; numberOfColumns--) {
let styleGivenNumberOfColumns: string[] = []
styleGivenNumberOfColumns.push(
css`
.column-layout-${i} {
column-count: ${numberOfColumns};
max-width: min(100%, ${columnWidthMax * numberOfColumns}px);
}
`,
columnLayouts.forEach((columnEntries) => {
for (let numberOfColumns = columnEntries.length; numberOfColumns >= 1; numberOfColumns--) {
const columnsIdMap = determineColumns(
columnEntries.map((columnEntry) => columnEntry.numberOfEntries),
numberOfColumns,
)
const columnsIdMap = determineColumns(columns, numberOfColumns)
const columnBreakPoints = determineColumnBreakPoints(columnsIdMap)
columnBreakPoints.forEach((columnBreakPoint, j) => {
const columnBreakPointAfter = columnBreakPoints[j + 1] ?? false
if (!columnBreakPoint && !columnBreakPointAfter) return
// - `break-before: column` isn't supported by Firefox
// - `margin-bottom: 100%` trick only works in Firefox.
// - TODO: apply `margin-bottom: 100%;` only for firefox as it breaks the layout in Chrome
styleGivenNumberOfColumns.push(
css`
.column-layout-${i} .column-layout-entry:nth-child(${j + 1}) {
${!columnBreakPoint ? '' : 'break-before: column; padding-top: 36px;'}
${!columnBreakPointAfter || /* TODO */ true ? '' : 'margin-bottom: 100%;'}
}
`,
)
columnEntries.forEach((columnEntry, i) => {
columnEntry.navItemLeader.isColumnLayoutElement ??= {}
columnEntry.navItemLeader.isColumnLayoutElement[numberOfColumns] = columnsIdMap[i]
})
{
assert(styleGivenNumberOfColumns.length > 0)
const getMaxWidth = (columns: number) => (columns + 1) * columnWidthMin - 1
const isFirst = numberOfColumns === 1
const isLast = numberOfColumns === columns.length
const query = [
!isFirst && `(min-width: ${getMaxWidth(numberOfColumns - 1) + 1}px)`,
!isLast && `(max-width: ${getMaxWidth(numberOfColumns)}px)`,
]
.filter(Boolean)
.join(' and ')
if (query) {
styleGivenNumberOfColumns = [`@container ${query} {`, ...styleGivenNumberOfColumns, `}`]
}
}
style += styleGivenNumberOfColumns.join('\n') + '\n'
}
})
return style
}

function determineColumnBreakPoints(columnsIdMap: number[]): boolean[] {
assert(columnsIdMap[0] === 0)
let columnGroupedIdBefore = 0
const columnBreakPoints = columnsIdMap.map((columnGroupedId) => {
assert(
[
//
columnGroupedIdBefore,
columnGroupedIdBefore + 1,
].includes(columnGroupedId),
)
const val = columnGroupedId !== columnGroupedIdBefore
columnGroupedIdBefore = columnGroupedId
return val
})
return columnBreakPoints
}

function determineColumns(columnsUnmerged: number[], numberOfColumns: number): number[] {
Expand Down
4 changes: 0 additions & 4 deletions src/renderer/onRenderHtml.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { assert } from '../utils/server'
import type { PageContextResolved } from '../config/resolvePageContext'
import { getPageElement } from './getPageElement'
import type { OnRenderHtmlAsync } from 'vike/types'
import { getStyleColumnLayout } from './getStyleColumnLayout'

const onRenderHtml: OnRenderHtmlAsync = async (
pageContext,
Expand All @@ -16,8 +15,6 @@ Promise<Awaited<ReturnType<OnRenderHtmlAsync>>> => {

const page = getPageElement(pageContext, pageContextResolved)

const styleMenuModalLayout = getStyleColumnLayout(pageContextResolved.columnLayouts)

const descriptionTag = pageContextResolved.isLandingPage
? dangerouslySkipEscape(`<meta name="description" content="${pageContextResolved.meta.tagline}" />`)
: ''
Expand All @@ -33,7 +30,6 @@ Promise<Awaited<ReturnType<OnRenderHtmlAsync>>> => {
${descriptionTag}
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
${getOpenGraphTags(pageContext.urlPathname, pageContextResolved.documentTitle, pageContextResolved.meta)}
<style>${dangerouslySkipEscape(styleMenuModalLayout)}</style>
<meta name="docsearch:category" content="${pageContextResolved.activeCategory}" />
</head>
<body>
Expand Down

0 comments on commit 0940201

Please sign in to comment.