blob: 211f6dcac503d1fff0e630acec67773b5f3a8e19 [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, css, html} from 'lit';
import {customElement, query, state} from 'lit/decorators';
import {assertIsDefined} from '../../../utils/common-util';
declare global {
interface HTMLElementTagNameMap {
'gr-page-nav': GrPageNav;
}
}
@customElement('gr-page-nav')
export class GrPageNav extends LitElement {
@query('nav') private nav?: HTMLElement;
// private but used in test
@state() headerHeight?: number;
private readonly bodyScrollHandler: () => void;
constructor() {
super();
this.bodyScrollHandler = () => this.handleBodyScroll();
}
override connectedCallback() {
super.connectedCallback();
window.addEventListener('scroll', this.bodyScrollHandler);
}
override disconnectedCallback() {
window.removeEventListener('scroll', this.bodyScrollHandler);
super.disconnectedCallback();
}
static override get styles() {
return [
sharedStyles,
css`
nav {
background-color: var(--table-header-background-color);
border: 1px solid var(--border-color);
border-top: none;
height: 100%;
position: absolute;
top: 0;
width: 14em;
}
nav.pinned {
position: fixed;
}
@media only screen and (max-width: 53em) {
nav {
display: none;
}
}
`,
];
}
override render() {
return html`
<nav aria-label="Sidebar">
<slot></slot>
</nav>
`;
}
// private but used in test
handleBodyScroll() {
assertIsDefined(this.nav, 'nav');
if (this.headerHeight === undefined) {
let top = this.getOffsetTop(this);
// TODO(TS): Element doesn't have offsetParent,
// while `offsetParent` are returning Element not HTMLElement
for (
let offsetParent = this.offsetParent as HTMLElement | undefined;
offsetParent;
offsetParent = this.getOffsetParent(offsetParent)
) {
top += this.getOffsetTop(offsetParent);
}
this.headerHeight = top;
}
this.nav.classList.toggle(
'pinned',
this.getScrollY() >= (this.headerHeight || 0)
);
}
/* Functions used for test purposes */
private getOffsetParent(element?: HTMLElement) {
if (!element || !('offsetParent' in element)) {
return undefined;
}
return element.offsetParent as HTMLElement;
}
// private but used in test
getOffsetTop(element: HTMLElement) {
return element.offsetTop;
}
// private but used in test
getScrollY() {
return window.scrollY;
}
}