|
--- |
|
|
|
--- |
|
|
|
<return-button class="scoping-root"> |
|
<a href="#top" class="button"> |
|
<span class="visually-hidden">本文上部へ戻る</span> |
|
</a> |
|
</return-button> |
|
|
|
<script> |
|
class ReturnToTopButton extends HTMLElement { |
|
private static readonly SENTINEL_BREAKPOINT = '30dvb' |
|
private static readonly ACTIVE_ATTR = 'display' |
|
|
|
private sentinel: HTMLElement | null = null |
|
private observer: IntersectionObserver | null = null |
|
|
|
constructor() { |
|
super() |
|
} |
|
|
|
connectedCallback(): void { |
|
this.sentinel = this.createSentinel(ReturnToTopButton.SENTINEL_BREAKPOINT) |
|
this.initializeObserver() |
|
} |
|
|
|
disconnectedCallback(): void { |
|
if (this.observer) this.observer.disconnect() |
|
if (this.sentinel) this.sentinel.remove() |
|
} |
|
|
|
private createSentinel(breakpoint: string): HTMLElement { |
|
const element = document.createElement('div') |
|
|
|
Object.assign(element.style, { |
|
position: 'absolute', |
|
insetBlockStart: breakpoint, |
|
inlineSize: '0', |
|
blockSize: '0', |
|
visibility: 'hidden', |
|
}) |
|
|
|
document.body.appendChild(element) |
|
|
|
return element |
|
} |
|
|
|
private initializeObserver(): void { |
|
if (!this.sentinel) return |
|
|
|
this.observer = new IntersectionObserver( |
|
(entries) => { |
|
const [entry] = entries |
|
this.setAttribute(ReturnToTopButton.ACTIVE_ATTR, String(!entry.isIntersecting)) |
|
}, |
|
{ |
|
threshold: 0, |
|
rootMargin: '0px', |
|
}, |
|
) |
|
|
|
this.observer.observe(this.sentinel) |
|
} |
|
} |
|
|
|
customElements.define('return-button', ReturnToTopButton) |
|
</script> |
|
|
|
<style> |
|
.scoping-root { |
|
container: return-button / inline-size; |
|
display: block flow; |
|
|
|
@media (scripting: enabled) { |
|
visibility: hidden; |
|
} |
|
|
|
&:defined { |
|
transition: visibility var(--duration-default); |
|
animation: var(--_keyframes) var(--duration-default) both; |
|
} |
|
|
|
&[display='true'] { |
|
--_keyframes: fade-in; |
|
|
|
visibility: unset; |
|
} |
|
|
|
&[display='false'] { |
|
--_keyframes: fade-out; |
|
} |
|
} |
|
|
|
.button { |
|
--_foreground: var(--is-hover-false, var(--foreground-button)) var(--is-hover-true, var(--foreground-button-active)); |
|
--_background: var(--is-hover-false, var(--background-button)) var(--is-hover-true, var(--background-button-active)); |
|
|
|
display: block grid; |
|
grid-template: var(--base-icon-size) / var(--base-icon-size); |
|
place-content: center; |
|
aspect-ratio: 1; |
|
border: 1px solid transparent; |
|
border-radius: var(--rounded-sm); |
|
box-shadow: var(--shadow-button); |
|
background-color: var(--_background); |
|
color: var(--_foreground); |
|
transition: |
|
background-color var(--duration-default), |
|
color var(--duration-default); |
|
-webkit-touch-callout: none; |
|
|
|
&::after { |
|
content: ''; |
|
mask-image: var(--icon-chevron-up); |
|
background-color: var(--background-current); |
|
} |
|
} |
|
</style> |