Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Dashboard: fade the section header subtitle out as the widgets scroll.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Dashboard: keep the section title and date controls in view while the widgets scroll.
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,96 @@ $section-header-title-min-inline-size: 200px;
* controls track either.
*/
grid-template-columns: minmax($section-header-title-min-inline-size, 1fr) minmax(0, max-content);
align-items: start;
align-items: center;

// One value on both axes: it separates the title from the controls beside it
// and from the subtitle below, stacked or not.
gap: var(--wpds-dimension-gap-sm);
// Column gap only: the rows carry their spacing as their own padding, so
// the condensing subtitle can take it along. A row gap belongs to the
// track and would stay behind as a band under the title.
column-gap: var(--wpds-dimension-gap-sm);
row-gap: 0;

.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.subtitle {
// A row wrapper rather than the text itself: condensing animates the grid
// row, and the padding is the row spacing it gives back on the way out.
.subtitleRow {
grid-column: 1 / -1;
padding-block-start: var(--wpds-dimension-gap-sm);
}
}

/*
* Condenses the header into a compact bar as it pins: the subtitle fades and
* gives its row back, and the title drops a step on the type scale.
*
* The surface, not this component, knows where the header pins, so it opts in
* by publishing a `--section-header-pin` view timeline whose `exit` phase
* opens at the pin point (see the dashboard's pin marker in
* `routes/dashboard/stage.module.scss`). Progress-driven, so it follows the
* scroll in both directions; the short range bounds the per-frame layout cost
* of animating the grid row, and `fr` interpolation collapses the row without
* measuring the subtitle text.
*
* The @supports gate is load-bearing: without scroll-driven animation
* support, the `animation` shorthand would run as a 0s time animation and its
* `both` fill would freeze the header in the condensed state. Inside the gate
* an unresolved timeline (a surface that publishes no marker) leaves the
* animation inert, which is the resting layout.
*/
@supports (animation-timeline: --section-header-pin) {

.condensing {

.subtitleRow {
display: grid;
grid-template-rows: 1fr;
animation: condense-subtitle linear both;
animation-timeline: --section-header-pin;
animation-range: exit;

> .subtitle {
min-block-size: 0;
overflow: hidden;
}
}

// Buys no height: the row is floored by the date controls beside it.
// What changes is what the pinned band reads as.
.title {
animation: condense-title linear both;
animation-timeline: --section-header-pin;
animation-range: exit;
}
}

// Scroll-driven, but still chrome moving under the reader.
@media (prefers-reduced-motion: reduce) {

.condensing .subtitleRow,
.condensing .title {
animation: none;
}
}
}

@keyframes condense-subtitle {

to {
grid-template-rows: 0fr;
opacity: 0;
padding-block-start: 0;
}
}

@keyframes condense-title {

to {
font-size: var(--wpds-typography-font-size-xl);
line-height: var(--wpds-typography-line-height-xl);
}
}

Expand Down Expand Up @@ -88,14 +164,16 @@ $section-header-title-min-inline-size: 200px;

// One column, so source order would leave the controls between the title and
// the subtitle describing it.
.subtitle {
.subtitleRow {
order: 1;
}

// Last row, reading from the same edge as the title instead of the far one.
// Last row, reading from the same edge as the title. A row of its own now,
// so it carries its row spacing as padding the way the subtitle does.
.controls {
order: 2;
justify-content: flex-start;
padding-block-start: var(--wpds-dimension-gap-sm);

:global(.date-filters-panel__row) {
justify-content: flex-start;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Stack, Text } from '@jetpack-premium-analytics/externals';
import clsx from 'clsx';
import { ReactNode } from 'react';
import styles from './section-header.module.scss';

Expand All @@ -11,6 +12,17 @@ type SectionHeaderProps = {
*/
subtitle?: string;

/**
* Condenses the header into a compact bar once it pins: the subtitle fades
* out and gives its row back, and the title drops a step on the type
* scale. Only for surfaces that pin this header, and the surface must also
* publish a `--section-header-pin` view timeline marking where it pins
* (see the dashboard's pin marker in `routes/dashboard/stage.module.scss`).
* Without that timeline, or without browser support for it, the header
* keeps its resting layout.
*/
condenseOnScroll?: boolean;

/**
* Date controls anchored to the end of the header row.
*/
Expand All @@ -32,9 +44,14 @@ type SectionHeaderProps = {
* @param {SectionHeaderProps} props - The props for the SectionHeader component.
* @return The section header element.
*/
export function SectionHeader( { title, subtitle, children }: SectionHeaderProps ) {
export function SectionHeader( {
title,
subtitle,
condenseOnScroll = false,
children,
}: SectionHeaderProps ) {
return (
<div className={ styles.container }>
<div className={ clsx( styles.container, condenseOnScroll && styles.condensing ) }>
<div className={ styles.layout }>
{ /* The `title` attribute is the only way back to a name the
ellipsis cut off. */ }
Expand All @@ -47,9 +64,11 @@ export function SectionHeader( { title, subtitle, children }: SectionHeaderProps
</Stack>

{ subtitle ? (
<Text className={ styles.subtitle } variant="body-md">
{ subtitle }
</Text>
<div className={ styles.subtitleRow }>
<Text className={ styles.subtitle } variant="body-md">
{ subtitle }
</Text>
</div>
) : null }
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,10 @@ function YearDateControls( {
type SectionHeaderStoryProps = {
title: string;
subtitle?: string;
condenseOnScroll?: boolean;
};

function RollingSectionHeaderStory( { title }: SectionHeaderStoryProps ) {
function RollingSectionHeaderStory( { title, condenseOnScroll }: SectionHeaderStoryProps ) {
const [ applied, setApplied ] = useState< AppliedDateState >( () => {
const initial = buildInitialPrimaryState();

Expand All @@ -292,7 +293,11 @@ function RollingSectionHeaderStory( { title }: SectionHeaderStoryProps ) {
} );

return (
<SectionHeader title={ title } subtitle={ getSectionSubtitle( applied ) }>
<SectionHeader
title={ title }
subtitle={ getSectionSubtitle( applied ) }
condenseOnScroll={ condenseOnScroll }
>
<RollingDateControls onAppliedChange={ setApplied } />
</SectionHeader>
);
Expand Down Expand Up @@ -376,6 +381,55 @@ export const Stacked: Story = {
),
};

/*
* Story-only stand-in for what a surface provides around a pinned header: a
* strip to scroll past and the pin marker publishing the view timeline. The
* dashboard does this in `routes/dashboard/stage.module.scss`.
*/
const PIN_TIMELINE = '--section-header-pin';
const PIN_MARKER_STYLE = {
position: 'absolute',
insetBlockStart: 0,
inlineSize: 1,
blockSize: 40,
visibility: 'hidden',
pointerEvents: 'none',
viewTimelineName: PIN_TIMELINE,
viewTimelineAxis: 'block',
} as const;

/**
* `condenseOnScroll` on a pinned header: scroll the box below, and once the
* header clears the strip above it and pins, the subtitle fades and gives its
* row back over the next 40px. Nothing condenses while the header is still on
* its way up, and the effect reverses as you scroll back up. Browsers without
* scroll-driven animations, surfaces that publish no pin marker, and readers
* who asked for reduced motion all keep the subtitle in place.
*/
export const CondensingOnScroll: Story = {
args: {
title: 'Site traffic',
},
render: ( { title } ) => (
<div style={ { blockSize: 320, overflowY: 'auto' } }>
<div style={ { blockSize: 48 } }>Something to scroll past, as the section tabs are.</div>
<div style={ { position: 'relative', timelineScope: PIN_TIMELINE } }>
<div style={ PIN_MARKER_STYLE } aria-hidden="true" />
<div
style={ {
position: 'sticky',
insetBlockStart: 0,
background: 'var(--wpds-color-background-surface-neutral-strong)',
} }
>
<RollingSectionHeaderStory title={ title } condenseOnScroll />
</div>
<div style={ { blockSize: 900 } } />
</div>
</div>
),
};

/**
* The left half on its own: nothing in the controls slot.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@
flex: 1 1 auto;
min-block-size: 0;

// The section's scroll container. Scrolling here rather than the boot
// stage keeps the page header out of the scroll flow entirely: `Page` is
// `height: 100%`, so a sticky header inside it runs out of containing
// block one viewport in and slides away. The tab list scrolls away with
// the content, which is interim until the sections move into `Page`'s
// navigation slot (WordPress/gutenberg#79746, not yet in
// `@wordpress/admin-ui`; WOOA7S-1931).
overflow-y: auto;

// Own stacking context, so in-panel layers (the sticky section header,
// widget focus rings) never compete with the page header above.
position: relative;
z-index: 0;
}

.tabList {
// Scoped under the root to outweigh the shared tab bar's own single-class rule
// rather than depend on which of the two stylesheets the bundle emits last.
.root .tabList {
padding-inline: var(--wpds-dimension-padding-2xl);
z-index: 0;

// The page header's fill: the tabs and the section header below are page
// chrome, so the whole block above the grid reads as one plane and the
// canvas begins where the widgets do.
background: var(--wpds-color-background-surface-neutral-strong);

// The gap under the tabs moves into the section header's fill.
margin-block-end: 0;
}
Loading
Loading