Skip to content

Commit fbb1d4f

Browse files
authored
feat(stickyHeader): ✨ re-implement sticky header (#1073)
Sticky header is reimplemented to be a separate HTML element to avoid a bunch of layout issue caused by the existing implementation. This is the first iteration and there will be upcoming changes to it. Fixes: #906
1 parent e859898 commit fbb1d4f

23 files changed

+726
-113
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare( strict_types=1 );
4+
5+
namespace MediaWiki\Skins\Citizen\Components;
6+
7+
/**
8+
* CitizenComponentButton component
9+
*
10+
* This implements the Codex CSS-only button component
11+
* Based on VectorComponentButton
12+
* @see https://doc.wikimedia.org/codex/main/components/demos/button.html
13+
*/
14+
class CitizenComponentButton implements CitizenComponent {
15+
16+
public function __construct(
17+
private string $label = '',
18+
private ?string $icon = null,
19+
private ?string $id = null,
20+
private ?string $class = null,
21+
private array $attributes = [],
22+
private string $weight = 'normal',
23+
private string $action = 'default',
24+
private string $size = 'medium',
25+
private bool $iconOnly = false,
26+
private ?string $href = null
27+
) {
28+
// Weight can only be normal, primary, or quiet
29+
if ( $this->weight !== 'primary' && $this->weight !== 'quiet' ) {
30+
$this->weight = 'normal';
31+
}
32+
// Action can only be default, progressive or destructive
33+
if ( $this->action !== 'progressive' && $this->action !== 'destructive' ) {
34+
$this->action = 'default';
35+
}
36+
// Size can only be medium or large
37+
if ( $this->size !== 'medium' && $this->size !== 'large' ) {
38+
$this->size = 'medium';
39+
}
40+
}
41+
42+
/**
43+
* Constructs button classes based on the props
44+
*/
45+
private function getClasses(): string {
46+
$classes = 'cdx-button';
47+
if ( $this->href ) {
48+
$classes .= ' cdx-button--fake-button cdx-button--fake-button--enabled';
49+
}
50+
switch ( $this->weight ) {
51+
case 'primary':
52+
$classes .= ' cdx-button--weight-primary';
53+
break;
54+
case 'quiet':
55+
$classes .= ' cdx-button--weight-quiet';
56+
break;
57+
}
58+
switch ( $this->action ) {
59+
case 'progressive':
60+
$classes .= ' cdx-button--action-progressive';
61+
break;
62+
case 'destructive':
63+
$classes .= ' cdx-button--action-destructive';
64+
break;
65+
}
66+
switch ( $this->size ) {
67+
case 'large':
68+
$classes .= ' cdx-button--size-large';
69+
break;
70+
}
71+
if ( $this->iconOnly ) {
72+
$classes .= ' cdx-button--icon-only';
73+
}
74+
if ( $this->class ) {
75+
$classes .= ' ' . $this->class;
76+
}
77+
return $classes;
78+
}
79+
80+
/**
81+
* @inheritDoc
82+
*/
83+
public function getTemplateData(): array {
84+
$arrayAttributes = [];
85+
foreach ( $this->attributes as $key => $value ) {
86+
if ( $value === null ) {
87+
continue;
88+
}
89+
$arrayAttributes[] = [ 'key' => $key, 'value' => $value ];
90+
}
91+
return [
92+
'label' => $this->label,
93+
'icon' => $this->icon,
94+
'id' => $this->id,
95+
'class' => $this->getClasses(),
96+
'href' => $this->href,
97+
'array-attributes' => $arrayAttributes
98+
];
99+
}
100+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare( strict_types=1 );
4+
5+
namespace MediaWiki\Skins\Citizen\Components;
6+
7+
/**
8+
* CitizenComponentStickyHeader component
9+
*/
10+
class CitizenComponentStickyHeader implements CitizenComponent {
11+
12+
private const SHARE_ICON = [
13+
'id' => 'citizen-share-sticky-header',
14+
'clickTarget' => '#citizen-share',
15+
'icon' => 'wikimedia-share'
16+
];
17+
18+
private const TALK_ICON = [
19+
'id' => 'ca-talk-sticky-header',
20+
'clickTarget' => '#ca-talk > a',
21+
'icon' => 'speechBubbles'
22+
];
23+
24+
private const SUBJECT_ICON = [
25+
'id' => 'ca-subject-sticky-header',
26+
'clickTarget' => '#ca-subject > a',
27+
'icon' => 'article'
28+
];
29+
30+
private const HISTORY_ICON = [
31+
'id' => 'ca-history-sticky-header',
32+
'clickTarget' => '#ca-history > a',
33+
'icon' => 'wikimedia-history'
34+
];
35+
36+
private const EDIT_VE_ICON = [
37+
'id' => 'ca-ve-edit-sticky-header',
38+
'clickTarget' => '#ca-ve-edit > a',
39+
'icon' => 'wikimedia-edit'
40+
];
41+
42+
private const EDIT_WIKITEXT_ICON = [
43+
'id' => 'ca-edit-sticky-header',
44+
'clickTarget' => '#ca-edit > a',
45+
'icon' => 'wikimedia-wikiText'
46+
];
47+
48+
private const EDIT_PROTECTED_ICON = [
49+
'id' => 'ca-viewsource-sticky-header',
50+
'clickTarget' => '#ca-viewsource > a',
51+
'icon' => 'wikimedia-editLock'
52+
];
53+
54+
public function __construct(
55+
private bool $visualEditorTabPositionFirst = false
56+
) {
57+
}
58+
59+
/**
60+
* Creates array of Button components in the sticky header
61+
*/
62+
private function getIconButtons(): array {
63+
$icons = [
64+
self::SHARE_ICON,
65+
self::HISTORY_ICON,
66+
$this->visualEditorTabPositionFirst ? self::EDIT_VE_ICON : self::EDIT_WIKITEXT_ICON,
67+
$this->visualEditorTabPositionFirst ? self::EDIT_WIKITEXT_ICON : self::EDIT_VE_ICON,
68+
self::EDIT_PROTECTED_ICON,
69+
self::TALK_ICON,
70+
self::SUBJECT_ICON
71+
];
72+
$iconButtons = [];
73+
foreach ( $icons as $icon ) {
74+
$button = new CitizenComponentButton(
75+
"",
76+
$icon[ 'icon' ],
77+
$icon[ 'id' ],
78+
$icon[ 'class' ] ?? '',
79+
[
80+
'tabindex' => '-1',
81+
'data-mw-citizen-click-target' => $icon[ 'clickTarget' ] ?? null,
82+
],
83+
'quiet',
84+
'default',
85+
'large',
86+
true,
87+
null
88+
);
89+
$iconButtons[] = $button->getTemplateData();
90+
}
91+
return $iconButtons;
92+
}
93+
94+
/**
95+
* @inheritDoc
96+
*/
97+
public function getTemplateData(): array {
98+
return [
99+
'array-icon-buttons' => $this->getIconButtons()
100+
];
101+
}
102+
}

includes/SkinCitizen.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use MediaWiki\Skins\Citizen\Components\CitizenComponentPageTools;
3333
use MediaWiki\Skins\Citizen\Components\CitizenComponentSearchBox;
3434
use MediaWiki\Skins\Citizen\Components\CitizenComponentSiteStats;
35+
use MediaWiki\Skins\Citizen\Components\CitizenComponentStickyHeader;
3536
use MediaWiki\Skins\Citizen\Components\CitizenComponentUserInfo;
3637
use MediaWiki\Skins\Citizen\Partials\BodyContent;
3738
use MediaWiki\Skins\Citizen\Partials\Metadata;
@@ -155,6 +156,9 @@ public function getTemplateData(): array {
155156
$title,
156157
$user,
157158
$parentData['data-portlets']['data-user-page']
159+
),
160+
'data-sticky-header' => new CitizenComponentStickyHeader(
161+
$this->isVisualEditorTabPositionFirst( $parentData['data-portlets']['data-views'] )
158162
)
159163
];
160164

@@ -165,16 +169,42 @@ public function getTemplateData(): array {
165169
}
166170
}
167171

172+
// HACK: So that we only get the tagline once
173+
$parentData['data-sticky-header']['html-tagline'] = $parentData['data-page-heading']['html-tagline'];
174+
168175
// HACK: So that we can use Icon.mustache in Header__logo.mustache
169176
$parentData['data-logos']['icon-home'] = 'home';
170177

178+
$isTocEnabled = !empty( $parentData['data-toc'][ 'array-sections' ] );
179+
if ( $isTocEnabled ) {
180+
$this->getOutput()->addBodyClasses( 'citizen-toc-enabled' );
181+
}
182+
171183
return array_merge( $parentData, [
172184
// Booleans
173-
'toc-enabled' => !empty( $parentData['data-toc'] ),
185+
'toc-enabled' => $isTocEnabled,
174186
'html-body-content--formatted' => $bodycontent->decorateBodyContent( $parentData['html-body-content'] )
175187
] );
176188
}
177189

190+
/**
191+
* Check whether Visual Editor Tab Position is first
192+
* From Vector 2022
193+
*
194+
* @param array $dataViews
195+
* @return bool
196+
*/
197+
private function isVisualEditorTabPositionFirst( array $dataViews ): bool {
198+
$names = [ 've-edit', 'edit' ];
199+
// find if under key 'name' 've-edit' or 'edit' is the before item in the array
200+
for ( $i = 0; $i < count( $dataViews[ 'array-items' ] ); $i++ ) {
201+
if ( in_array( $dataViews[ 'array-items' ][ $i ][ 'name' ], $names ) ) {
202+
return $dataViews[ 'array-items' ][ $i ][ 'name' ] === $names[ 0 ];
203+
}
204+
}
205+
return false;
206+
}
207+
178208
/**
179209
* @inheritDoc
180210
*

resources/mixins.less

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565

6666
// Header card popups
6767
.mixin-citizen-header-card( @position ) {
68-
position: absolute;
6968
right: 0;
7069
bottom: 100%;
7170
left: 0;

resources/skins.citizen.scripts/setupObservers.js

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Adopted from Vector 2022
22
const
33
scrollObserver = require( './scrollObserver.js' ),
4-
resizeObserver = require( './resizeObserver.js' ),
54
initSectionObserver = require( './sectionObserver.js' ),
65
stickyHeader = require( './stickyHeader.js' ),
76
initTableOfContents = require( './tableOfContents.js' ),
@@ -187,13 +186,11 @@ const main = () => {
187186

188187
const
189188
stickyHeaderElement = document.getElementById( stickyHeader.STICKY_HEADER_ID ),
190-
stickyIntersection = document.getElementById( 'citizen-page-header-sticky-sentinel' ),
191-
stickyPlaceholder = document.getElementById( stickyHeader.STICKY_HEADER_PLACEHOLDER_ID );
189+
stickyIntersection = document.getElementById( 'citizen-page-header-sticky-sentinel' );
192190

193191
// eslint-disable-next-line es-x/no-optional-chaining
194192
const shouldStickyHeader = getComputedStyle( stickyIntersection )?.getPropertyValue( 'display' ) !== 'none';
195193
const isStickyHeaderAllowed = !!stickyHeaderElement &&
196-
!!stickyPlaceholder &&
197194
!!stickyIntersection &&
198195
shouldStickyHeader;
199196

@@ -209,13 +206,17 @@ const main = () => {
209206
10
210207
);
211208

209+
if ( isStickyHeaderAllowed ) {
210+
stickyHeader.init( stickyHeaderElement );
211+
}
212+
212213
const resumeStickyHeader = () => {
213214
if (
214215
isStickyHeaderAllowed &&
215216
!document.body.classList.contains( stickyHeader.STICKY_HEADER_VISIBLE_CLASS ) &&
216217
document.body.classList.contains( PAGE_TITLE_INTERSECTION_CLASS )
217218
) {
218-
stickyHeader.show( stickyHeaderElement, stickyPlaceholder );
219+
stickyHeader.show( stickyHeaderElement );
219220
if ( document.documentElement.classList.contains( 'citizen-feature-autohide-navigation-clientpref-1' ) ) {
220221
scrollDirectionObserver.resume();
221222
}
@@ -224,7 +225,7 @@ const main = () => {
224225

225226
const pauseStickyHeader = () => {
226227
if ( document.body.classList.contains( stickyHeader.STICKY_HEADER_VISIBLE_CLASS ) ) {
227-
stickyHeader.hide( stickyHeaderElement, stickyPlaceholder );
228+
stickyHeader.hide( stickyHeaderElement );
228229
scrollDirectionObserver.pause();
229230
}
230231
};
@@ -242,39 +243,6 @@ const main = () => {
242243

243244
pageHeaderObserver.observe( stickyIntersection );
244245

245-
// Initialize var
246-
let bodyWidth = 0;
247-
const bodyObserver = resizeObserver.initResizeObserver(
248-
// onResize
249-
() => {},
250-
// onResizeStart
251-
( entry ) => {
252-
// eslint-disable-next-line es-x/no-optional-chaining
253-
bodyWidth = entry.borderBoxSize?.[ 0 ].inlineSize;
254-
// Disable all CSS animation during resize
255-
if ( document.documentElement.classList.contains( 'citizen-animations-ready' ) ) {
256-
document.documentElement.classList.remove( 'citizen-animations-ready' );
257-
}
258-
},
259-
// onResizeEnd
260-
( entry ) => {
261-
// eslint-disable-next-line es-x/no-optional-chaining
262-
const newBodyWidth = entry.borderBoxSize?.[ 0 ].inlineSize;
263-
const shouldRecalcStickyHeader =
264-
document.body.classList.contains( PAGE_TITLE_INTERSECTION_CLASS ) &&
265-
typeof newBodyWidth === 'number' &&
266-
bodyWidth !== newBodyWidth;
267-
268-
// Enable CSS animation after resize is finished
269-
document.documentElement.classList.add( 'citizen-animations-ready' );
270-
// Recalculate sticky header height at the end of the resize
271-
if ( shouldRecalcStickyHeader ) {
272-
resumeStickyHeader();
273-
}
274-
}
275-
);
276-
bodyObserver.observe( document.body );
277-
278246
mw.hook( 've.activationStart' ).add( () => {
279247
pauseStickyHeader();
280248
} );

0 commit comments

Comments
 (0)