CLARIN-DSpace v9/Port #1350 (admin-sidebar gutter via CSS custom properties) to the v9 base - #1512
Conversation
…er via CSS var (no logged-in reload shift)
For an authenticated user a hard reload shifted the whole page right by the
admin-sidebar width. The `.outer-wrapper` left gutter was produced by the
`@slideSidebarPadding` animation, whose width comes from `CSSVariableService`,
a browser-only store fed from `document.styleSheets`. On the server that store
is empty, so SSR emitted `style="padding-left: 0;"` and the browser resolved the
real width afterwards - hence the jump. Measured on dev-6 before this change:
<div _ngcontent-... class="outer-wrapper ng-tns-c4194216939-0 ng-trigger
ng-trigger-slideSidebarPadding" style="padding-left: 0;">
The gutter now comes from a CSS class - `ds-admin-sidebar-{hidden,unpinned,pinned}`,
derived from a small `sidebarPaddingState$` - whose `padding-left` resolves from
the `--ds-admin-sidebar-*` custom properties. Those are defined in the
render-blocking theme stylesheet (`--ds-admin-sidebar-fixed-element-width: 55px`,
`--ds-admin-sidebar-total-width: 305px` on dev-6), so CSS resolves the gutter
identically on the server and in the browser, with no hardcoded width and no
browser-only variable read. The pin/unpin slide is preserved by
`transition: padding-left 300ms`, gated behind `ds-admin-sidebar-animate`, which
`ngAfterViewInit` enables only after the first paint so the initial SSR->CSR
resolution never animates.
Adaptations to the v9 base (the source commit targets the 7.x root component):
* `.outer-wrapper` on 9-base already carries `[ngClass]="browserOsClasses…"`, a
vanilla-9 feature 7.x does not have. Angular allows one `[ngClass]` per
element, and overwriting it would silently drop `browser-firefox` /
`browser-firefox-windows`, which `_custom_variables.scss` uses. The two are
therefore merged into a single `outerWrapperClasses$` stream instead of
pasting the source's `[ngClass]="'ds-admin-sidebar-' + (…)"`. A unit test
guards the merge.
* the sidebar element is `<ds-admin-sidebar>` on v9, not
`<ds-themed-admin-sidebar>`; that context line is left as it is.
* the source's trailing-comma reformat of the `windowService` constructor
parameter is already present on 9-base, and `AfterViewInit` joins a
multi-line `@angular/core` import, so those two hunks are no-ops here.
`slideSidebarPadding` itself is deliberately left in
`src/app/shared/animations/slide.ts`: after this change it still has one
consumer, the `custom` theme's `file-section.component.ts`.
Two unit tests are added beyond the source commit, which ships none. The v9
`root.component.spec.ts` had a single `it('should create')` that cannot detect
this regression. Both new tests were proven load-bearing with negative controls
(see the PR description).
Source: 1321b8a (dtq-dev PR #1350)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Local gates (Windows, Node 20.19.0, worktree
|
|
CI is green on The remaining gate is the live one (card AC-FE-13-3 / manual scenario UNIVERSAL-028), which needs this merged and deployed to 8603. |
What
Ports
dataquest-dev/dspace-angular#1350 (1321b8acc0, "UFAL/Backport #1333: admin-sidebargutter via CSS var (no logged-in reload shift)") to
dtq-dev-9-base. Sync card FE-13 (tranche T3).For an authenticated user, a hard reload shifted the whole page right by the admin-sidebar width.
The
.outer-wrapperleft gutter was produced by the@slideSidebarPaddinganimation, whoseparamscome from
CSSVariableService— a browser-only store populated fromdocument.styleSheets. UnderSSR that store is empty, so the server emits
padding-left: 0and the browser resolves the real widthonly afterwards. Measured on the current deployment (dev-6:8603, anonymous
/home):The gutter is now a CSS class —
ds-admin-sidebar-{hidden,unpinned,pinned}, derived from a smallsidebarPaddingState$— whosepadding-leftresolves from the--ds-admin-sidebar-*customproperties. Those live in the render-blocking theme stylesheet, so CSS resolves the gutter
identically on the server and in the browser, with no hardcoded width and no browser-only read:
The pin/unpin slide is preserved by
transition: padding-left 300ms ease-in-out, gated behindds-admin-sidebar-animate, whichngAfterViewInitenables only after the first paint — so the initialSSR→CSR gutter resolution never animates.
Changes
app/root/root.component.tsslideSidebarPaddingimport +animations:entry removed;sidebarPaddingState$,outerWrapperClasses$,gutterTransitionEnabledadded;implements AfterViewInitwith arequestAnimationFramefirst-paint hookapp/root/root.component.html[@slideSidebarPadding]replaced by[class.ds-admin-sidebar-animate]plus a merged[ngClass]app/root/root.component.scss.outer-wrapper.ds-admin-sidebar-{hidden,unpinned,pinned,animate}rules appendedthemes/custom/app/root/root.component.tsslideSidebarPaddinglines dropped (parity with the source commit)app/root/root.component.spec.tssrc/app/root/andsrc/themes/custom/app/root/on this branch are byte-identical to vanilladspace-9.3(git diff dspace-9.3 origin/dtq-dev-9-base -- src/app/root/→ empty), so this fixes avanilla-v9 bug locally. Flagged for the human upstream triage; nothing was sent upstream.
Three adaptations from the 7.x hunks, spelled out
[ngClass]already exists here — merged, not replaced. 9-base's.outer-wrappercarries[ngClass]="browserOsClasses.asObservable() | async", which 7.x does not have. Angular allows one[ngClass]per element, and pasting the source's[ngClass]="'ds-admin-sidebar-' + (sidebarPaddingState$ | async)"would silently dropbrowser-firefox/browser-firefox-windows, which_custom_variables.scssuses(
.browser-firefox-windows { --ds-dark-scrollbar-width: 20px }). Hence one mergedouterWrapperClasses$stream. This is not hypothetical — see negative control NC-2 below,where pasting the source hunk verbatim really does render
[ 'outer-wrapper', 'ds-admin-sidebar-hidden' ]with the browser classes gone.<ds-admin-sidebar>, not<ds-themed-admin-sidebar>— the source hunk's context line is the7.x tag; this branch's tag is left unchanged.
windowServiceconstructor parameter is already present, and
AfterViewInitjoins an already multi-line@angular/coreimport. Net: the source's40+/5−in the TS becomes54+/3−, the +14 being theouterWrapperClasses$declaration (7) and assignment (6) plus a 3-linerequestAnimationFramebody instead of the source's 1-line one.
slideSidebarPaddingis deliberately kept insrc/app/shared/animations/slide.ts: after thischange it still has one consumer,
src/themes/custom/app/item-page/simple/field-components/file-section/file-section.component.ts:6,17.Tests added beyond the source commit
The source commit adds no spec, and this branch's
root.component.spec.tshad a singleit('should create')— a gate that cannot detect this regression. Two cases added:should emit the hidden gutter state when the admin sidebar is not visiblesidebarPaddingState$emits'hidden';outerWrapperClasses$equals['browser-firefox', 'browser-firefox-windows', 'ds-admin-sidebar-hidden'];.outer-wrappercarries all three in the DOMshould enable the gutter transition only after the first paint.outer-wrappercarriesds-admin-sidebar-pinnedimmediately but notds-admin-sidebar-animate;ngAfterViewInitregistered arequestAnimationFramecallback; invoking it flips both the flag and the classThe second test stubs
requestAnimationFrameand drives the callback by hand, so it does not dependon a real paint landing between jasmine callbacks.
Testing
Negative controls (production code broken one at a time, tests untouched; each gave
TOTAL: 1 FAILED, 2 SUCCESS):!visible → 'hidden'guard →Expected 'pinned' to equal 'hidden'.[ngClass]instead of merging →Expected [ 'outer-wrapper', 'ds-admin-sidebar-hidden' ] to contain 'browser-firefox'.gutterTransitionEnabled = trueinitially →Expected true to be false.plus… not to contain 'ds-admin-sidebar-animate'.requestAnimationFramehook →Expected 0 to be greater than 0.plus… to contain 'ds-admin-sidebar-animate'.Lint, circular-dependency check, production build and the server-side render check are pasted in the
gate comment below.
Live verification (card AC-FE-13-3 / manual scenario UNIVERSAL-028) is pending deployment. Note the
card's own probe is a permanent false pass and must be replaced:
grep -c 'ds-admin-sidebar-'alreadyreturns
1today, because the SSR<style>block inlines eight--ds-admin-sidebar-*propertynames and
grep -ccounts lines in a 517871-byte, 21-line document. The discriminating probe is:Source:
1321b8acc0(dtq-dev PR #1350). Sync card: FE-13.🤖 Generated with Claude Code