mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-10 04:22:06 +00:00
chore: rename article, remove unused sections
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
{
|
{
|
||||||
title: "Portals",
|
title: "Why is z-index not working?! - Explaining CSS Stacking Context",
|
||||||
description: "",
|
description: "",
|
||||||
published: '2023-01-01T22:12:03.284Z',
|
published: '2023-01-01T22:12:03.284Z',
|
||||||
authors: ['crutchcorn'],
|
authors: ['crutchcorn'],
|
||||||
tags: ['webdev'],
|
tags: ['webdev', 'css', 'html'],
|
||||||
attached: [],
|
attached: [],
|
||||||
order: 15,
|
license: 'cc-by-4'
|
||||||
series: "The Framework Field Guide"
|
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
|
|
||||||
While building sufficiently useful modals can be a challenging task, a rudimentary modal can be completed even without JavaScript.
|
While building sufficiently useful modals can be a challenging task, a rudimentary modal can be completed even without JavaScript.
|
||||||
|
|
||||||
While we'll loop back to JavaScript (using React, Angular, and Vue) in a bit, let's use some CSS and HTML in order to build a basic modal:
|
Let's use some CSS and HTML in order to build a basic modal:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div>
|
<div>
|
||||||
@@ -240,7 +239,7 @@ Notice how the purple box seemingly remains on "top" when we re-arrange the CSS
|
|||||||
|
|
||||||
Well...
|
Well...
|
||||||
|
|
||||||
### Re-arrange HTML Elements to Change the Stacking Order
|
# Re-arrange HTML Elements to Change the Stacking Order
|
||||||
|
|
||||||
Let's take the HTML we had before, and re-arrange it a bit:
|
Let's take the HTML we had before, and re-arrange it a bit:
|
||||||
|
|
||||||
@@ -258,7 +257,7 @@ Now if we look at the box order, we'll see...
|
|||||||
|
|
||||||
Now our boxes have reversed their height order! This is because one of the deciding factors of an element's `z` position is its relationship to other elements.
|
Now our boxes have reversed their height order! This is because one of the deciding factors of an element's `z` position is its relationship to other elements.
|
||||||
|
|
||||||
### Positioned Elements Behave Differently Than Non-Positioned Elements
|
# Positioned Elements Behave Differently Than Non-Positioned Elements
|
||||||
|
|
||||||
> This is where things get confusing. Take your time with this chapter, it's okay to have to re-read this section multiple times.
|
> This is where things get confusing. Take your time with this chapter, it's okay to have to re-read this section multiple times.
|
||||||
|
|
||||||
@@ -343,7 +342,7 @@ While our green button now smoothly moves left when you hover over it, there's a
|
|||||||
|
|
||||||
This is because positioning an element introduces a "stacked context". This means that our `relative` positioned element takes priority in the `z` layer over non-positioned elements.
|
This is because positioning an element introduces a "stacked context". This means that our `relative` positioned element takes priority in the `z` layer over non-positioned elements.
|
||||||
|
|
||||||
### Understanding more rules of Stacked Contexts
|
# Understanding more rules of Stacked Contexts
|
||||||
|
|
||||||
While `relative` positioning is one way that you can take priority in a stacked context, it's far from the only way to do so. Here's a list of CSS rules that will take priority in a stacked context, from the lowest priority to the highest priority:
|
While `relative` positioning is one way that you can take priority in a stacked context, it's far from the only way to do so. Here's a list of CSS rules that will take priority in a stacked context, from the lowest priority to the highest priority:
|
||||||
|
|
||||||
@@ -401,7 +400,7 @@ You would see the following order of elements:
|
|||||||
|
|
||||||
This is because the `lime` and `slate` take priority over `yellow` and `cyan` thanks to their `relative` positioning, but are still in HTML order within the same `z` level priority and within the same stacking context.
|
This is because the `lime` and `slate` take priority over `yellow` and `cyan` thanks to their `relative` positioning, but are still in HTML order within the same `z` level priority and within the same stacking context.
|
||||||
|
|
||||||
### Creating Stacking Contexts
|
# Creating Stacking Contexts
|
||||||
|
|
||||||
> "Welp, that's enough reading in the book today"
|
> "Welp, that's enough reading in the book today"
|
||||||
|
|
||||||
@@ -523,17 +522,17 @@ Then it will show "Absolute" above "Opacity", thanks to the order of the HTML se
|
|||||||
|
|
||||||
If we remove the `opacity: 0.99` from the `"Opacity"` `div`, then `"Absolute`" will be on top.
|
If we remove the `opacity: 0.99` from the `"Opacity"` `div`, then `"Absolute`" will be on top.
|
||||||
|
|
||||||
### Stacking Stacking Contexts
|
# Stacking Stacking Contexts
|
||||||
|
|
||||||
While the previous sections have been head scratchers, let's dive into mind melting territory: You can contain stacking contexts within other stacking contexts. 🤯
|
While the previous sections have been head scratchers, let's dive into mind melting territory: You can contain stacking contexts within other stacking contexts. 🤯
|
||||||
|
|
||||||
|
// TODO: Write
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# The Problem with Stacking Contexts
|
||||||
|
|
||||||
### The Problem with Stacking Contexts
|
// TODO: Explain that `z-index` cannot escape
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -545,368 +544,3 @@ While the previous sections have been head scratchers, let's dive into mind melt
|
|||||||
> - [What No One Told You About Z-Index - Philip Walton](https://philipwalton.com/articles/what-no-one-told-you-about-z-index/)
|
> - [What No One Told You About Z-Index - Philip Walton](https://philipwalton.com/articles/what-no-one-told-you-about-z-index/)
|
||||||
> - [Appendix E. Elaborate description of Stacking Contexts - W3C](https://www.w3.org/TR/CSS2/zindex.html)
|
> - [Appendix E. Elaborate description of Stacking Contexts - W3C](https://www.w3.org/TR/CSS2/zindex.html)
|
||||||
|
|
||||||
# What is a JavaScript portals?
|
|
||||||
|
|
||||||
> What does any of that CSS stuff have to do with my JavaScript?!
|
|
||||||
|
|
||||||
First: Tone. Second: Everything.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Using Local Portals
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- tabs:start -->
|
|
||||||
|
|
||||||
## React
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import React, { useMemo, useState } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const [portalRef, setPortalRef] = useState(null);
|
|
||||||
|
|
||||||
const portal = useMemo(() => {
|
|
||||||
if (!portalRef) return null;
|
|
||||||
return ReactDOM.createPortal(<div>Hello, world!</div>, portalRef);
|
|
||||||
}, [portalRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
ref={(el) => setPortalRef(el)}
|
|
||||||
style={{ height: '100px', width: '100px', border: '2px solid black' }}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
{portal}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Angular
|
|
||||||
|
|
||||||
While the other frameworks have something akin to a portal system built into their frameworks' core, Angular does not. Instead, the Angular team maintains a library called "Angular CDK" in order to have shared UI code for utilities such as portals.
|
|
||||||
|
|
||||||
To use the Angular CDK, you'll first need to install it into your project:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm i @angular/cdk
|
|
||||||
```
|
|
||||||
|
|
||||||
From here, we can import components and utilities directly from the CDK.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { PortalModule } from '@angular/cdk/portal';
|
|
||||||
import { DomPortal } from '@angular/cdk/portal';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-app',
|
|
||||||
template: `
|
|
||||||
<div style="height: 100px; width: 100px; border: 2px solid black;">
|
|
||||||
<ng-template [cdkPortalOutlet]="domPortal"></ng-template>
|
|
||||||
</div>
|
|
||||||
<div #portalContent>Hello, world!</div>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class AppComponent implements AfterViewInit {
|
|
||||||
@ViewChild('portalContent') portalContent: ElementRef<HTMLElement>;
|
|
||||||
|
|
||||||
domPortal: DomPortal<any>;
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
// This is to avoid an:
|
|
||||||
// "Expression has changed after it was checked"
|
|
||||||
// error when trying to set domPortal
|
|
||||||
setTimeout(() => {
|
|
||||||
this.domPortal = new DomPortal(this.portalContent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [AppComponent],
|
|
||||||
imports: [BrowserModule, PortalModule],
|
|
||||||
providers: [],
|
|
||||||
bootstrap: [AppComponent],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering `ng-template`
|
|
||||||
|
|
||||||
There might be a flash of the `div` on screen before our `ngAfterViewInit` occurs. As such, we may want to use an `ng-template`:
|
|
||||||
|
|
||||||
// TODO: Write
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { PortalModule, TemplatePortal } from '@angular/cdk/portal';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-app',
|
|
||||||
template: `
|
|
||||||
<div style="height: 100px; width: 100px; border: 2px solid black;">
|
|
||||||
<ng-template [cdkPortalOutlet]="domPortal"></ng-template>
|
|
||||||
</div>
|
|
||||||
<ng-template #portalContent>Hello, this is a template portal</ng-template>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class AppComponent implements AfterViewInit {
|
|
||||||
@ViewChild('portalContent') portalContent: TemplateRef<unknown>;
|
|
||||||
|
|
||||||
viewContainerRef = inject(ViewContainerRef);
|
|
||||||
domPortal: TemplatePortal<any>;
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
// This is to avoid an:
|
|
||||||
// "Expression has changed after it was checked"
|
|
||||||
// error when trying to set domPortal
|
|
||||||
setTimeout(() => {
|
|
||||||
this.domPortal = new TemplatePortal(
|
|
||||||
this.portalContent,
|
|
||||||
this.viewContainerRef
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Vue
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- App.vue -->
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const portalContainerEl = ref(null)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div style="height: 100px; width: 100px; border: 2px solid black">
|
|
||||||
<div ref="portalContainerEl"></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="portalContainerEl">
|
|
||||||
<Teleport :to="portalContainerEl">Hello, world!</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
We need this `v-if` in order to ensure that `portalContainerEl` has already been rendered and is ready to project content.
|
|
||||||
|
|
||||||
<!-- tabs:end -->
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Application-Wide Portals
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
<!-- tabs:start -->
|
|
||||||
|
|
||||||
## React
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import React, { useState, createContext, useContext } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
// We start by creating a context name
|
|
||||||
const PortalContext = React.createContext();
|
|
||||||
|
|
||||||
function ChildComponent() {
|
|
||||||
const portalRef = useContext(PortalContext);
|
|
||||||
if (!portalRef) return null;
|
|
||||||
return ReactDOM.createPortal(<div>Hello, world!</div>, portalRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
const [portalRef, setPortalRef] = useState(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PortalContext.Provider value={portalRef}>
|
|
||||||
<div
|
|
||||||
ref={(el) => setPortalRef(el)}
|
|
||||||
style={{ height: '100px', width: '100px', border: '2px solid black' }}
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<ChildComponent />
|
|
||||||
</PortalContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Angular
|
|
||||||
|
|
||||||
We can use a basic service to share our instance of a `Portal` between multiple components, parent and child alike.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Portal, PortalModule, TemplatePortal } from '@angular/cdk/portal';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
class PortalService {
|
|
||||||
portal: Portal<any> | null = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'modal',
|
|
||||||
template: `
|
|
||||||
<ng-template #portalContent>Test</ng-template>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class ModalComponent implements OnDestroy {
|
|
||||||
@ViewChild('portalContent') portalContent: TemplateRef<unknown>;
|
|
||||||
|
|
||||||
viewContainerRef = inject(ViewContainerRef);
|
|
||||||
domPortal: TemplatePortal<any>;
|
|
||||||
|
|
||||||
portalService = inject(PortalService);
|
|
||||||
|
|
||||||
ngAfterViewInit() {
|
|
||||||
// This is to avoid an:
|
|
||||||
// "Expression has changed after it was checked"
|
|
||||||
// error when trying to set domPortal
|
|
||||||
setTimeout(() => {
|
|
||||||
this.portalService.portal = new TemplatePortal(
|
|
||||||
this.portalContent,
|
|
||||||
this.viewContainerRef
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.portalService = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'my-app',
|
|
||||||
template: `
|
|
||||||
<div style="height: 100px; width: 100px; border: 2px solid black;" *ngIf="portalService.portal">
|
|
||||||
<ng-template [cdkPortalOutlet]="portalService.portal"></ng-template>
|
|
||||||
</div>
|
|
||||||
<modal></modal>
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
class AppComponent {
|
|
||||||
portalService = inject(PortalService);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Vue
|
|
||||||
|
|
||||||
```
|
|
||||||
<!-- App.vue -->
|
|
||||||
<script setup>
|
|
||||||
import { ref, provide } from 'vue'
|
|
||||||
import Child from './Child.vue'
|
|
||||||
|
|
||||||
const portalContainerEl = ref(null)
|
|
||||||
provide('portalContainerEl', portalContainerEl)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div style="height: 100px; width: 100px; border: 2px solid black">
|
|
||||||
<div ref="portalContainerEl"></div>
|
|
||||||
</div>
|
|
||||||
<Child />
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- tabs:end -->
|
|
||||||
|
|
||||||
// TODO: Write this
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# HTML-Wide Portals
|
|
||||||
|
|
||||||
// TODO: Write
|
|
||||||
|
|
||||||
<!-- tabs:start -->
|
|
||||||
|
|
||||||
## React
|
|
||||||
|
|
||||||
// TODO: Write
|
|
||||||
|
|
||||||
Alternatively, `ReactDOM.createPortal` supports passing an arbitrary HTML DOM node, such as `html.body`:
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
function ChildComponent() {
|
|
||||||
const bodyEl = useMemo(() => {
|
|
||||||
return document.querySelector('body');
|
|
||||||
}, []);
|
|
||||||
return ReactDOM.createPortal(<div>Hello, world!</div>, bodyEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return <ChildComponent />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Angular
|
|
||||||
|
|
||||||
// TODO: Write
|
|
||||||
|
|
||||||
Can't do this
|
|
||||||
|
|
||||||
## Vue
|
|
||||||
|
|
||||||
// TODO: Write
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- Child.vue -->
|
|
||||||
<script setup></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Teleport to="body">Hello, world!</Teleport>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- App.vue -->
|
|
||||||
<script setup>
|
|
||||||
import Child from './Child.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Child />
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- tabs:end -->
|
|
||||||
|
|||||||
Reference in New Issue
Block a user