fixed changelog conflict

This commit is contained in:
Kelsey Jackson
2025-09-09 11:53:50 -05:00
16 changed files with 624 additions and 63 deletions

View File

@@ -76,6 +76,7 @@ export default async function (eleventyConfig) {
//
eleventyConfig.addGlobalData('package', packageData);
eleventyConfig.addGlobalData('layout', 'page.njk');
eleventyConfig.addGlobalData('pageType', 'docs'); // Default page type
eleventyConfig.addGlobalData('server', {
head: '',
loginOrAvatar: '',

View File

@@ -15,8 +15,14 @@
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar.js"></script>{% endif %}
<script defer data-domain="webawesome.com" src="https://plausible.io/js/script.js"></script>
{# Docs styles #}
<link rel="stylesheet" href="/assets/styles/docs.css" />
{% if pageType == 'marketing' %}
{# Marketing styles #}
<link rel="stylesheet" href="/assets/styles/marketing.css" />
{% else %}
{# Docs styles (default) #}
<link rel="stylesheet" href="/assets/styles/docs.css" />
{% endif %}
{% block head %}{% endblock %}
@@ -42,51 +48,55 @@
}
</script>
</head>
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
<body class="layout-{{ layout | stripExtension }} page-{{ page.fileSlug or 'home' }}{{ ' page-wide' if wide }} page-{{ pageType or 'docs' }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop" disable-navigation-toggle mobile-breakpoint="1180">
<header slot="header" class="wa-split">
{# Logo #}
<div id="docs-branding">
{# Nav toggle #}
<wa-button appearance="plain" size="small" data-toggle-nav>
<wa-icon name="bars" label="Toggle navigation"></wa-icon>
</wa-button>
<wa-page view="desktop" disable-navigation-toggle {% if pageType == 'marketing' %}disable-sticky="header"{% endif %} mobile-breakpoint="1180">
{% if pageHeader %}
{% include pageHeader %}
{% else %}
<header slot="header" class="wa-split">
{# Logo #}
<div id="docs-branding">
{# Nav toggle #}
<wa-button appearance="plain" size="small" data-toggle-nav>
<wa-icon name="bars" label="Toggle navigation"></wa-icon>
</wa-button>
<a href="/" aria-label="Web Awesome">
<span class="wa-desktop-only">{% include "logo.njk" %}</span>
<span class="wa-mobile-only">{% include "logo-simple.njk" %}</span>
</a>
<small id="version-number" class="wa-desktop-only">{{ package.version }}</small>
<wa-badge variant="brand" appearance="filled" class="wa-desktop-only">Beta</wa-badge>
</div>
<div id="docs-toolbar" class="wa-cluster">
{# Desktop selectors #}
<div class="wa-desktop-only wa-cluster wa-gap-xs">
{% include "theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
<a href="/" aria-label="Web Awesome">
<span class="wa-desktop-only">{% include "logo.njk" %}</span>
<span class="wa-mobile-only">{% include "logo-simple.njk" %}</span>
</a>
<small id="version-number" class="wa-desktop-only">{{ package.version }}</small>
<wa-badge variant="brand" appearance="filled" class="wa-desktop-only">Beta</wa-badge>
</div>
<wa-divider orientation="vertical" class="wa-desktop-only"></wa-divider>
<div id="docs-toolbar" class="wa-cluster">
{# Desktop selectors #}
<div class="wa-desktop-only wa-cluster wa-gap-xs">
{% include "theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
<div id="github-buttons" class="wa-cluster wa-gap-xs">
<wa-button id="github-repo-button" href="https://github.com/shoelace-style/webawesome" rel="noopener noreferrer" target="_blank" appearance="filled" size="small">
<wa-icon family="brands" name="github" label="GitHub"></wa-icon>
</wa-button>
<wa-tooltip for="github-repo-button" distance="2">GitHub</wa-tooltip>
<wa-button id="github-star-button" href="https://github.com/shoelace-style/webawesome/stargazers" rel="noopener noreferrer" target="_blank" appearance="filled" size="small">
<wa-icon name="star" variant="regular" label="Star this repository"></wa-icon>
</wa-button>
<wa-tooltip for="github-star-button" distance="2">Star this repository</wa-tooltip>
<wa-divider orientation="vertical" class="wa-desktop-only"></wa-divider>
<div id="github-buttons" class="wa-cluster wa-gap-xs">
<wa-button id="github-repo-button" href="https://github.com/shoelace-style/webawesome" rel="noopener noreferrer" target="_blank" appearance="filled" size="small">
<wa-icon family="brands" name="github" label="GitHub"></wa-icon>
</wa-button>
<wa-tooltip for="github-repo-button" distance="2">GitHub</wa-tooltip>
<wa-button id="github-star-button" href="https://github.com/shoelace-style/webawesome/stargazers" rel="noopener noreferrer" target="_blank" appearance="filled" size="small">
<wa-icon name="star" variant="regular" label="Star this repository"></wa-icon>
</wa-button>
<wa-tooltip for="github-star-button" distance="2">Star this repository</wa-tooltip>
</div>
<wa-divider orientation="vertical"></wa-divider>
{# Login #}
{% server "loginOrAvatar" %}
</div>
<wa-divider orientation="vertical"></wa-divider>
{# Login #}
{% server "loginOrAvatar" %}
</div>
</header>
</header>
{% endif %}
{# Sidebar #}
{% if hasSidebar %}
@@ -138,6 +148,11 @@
</main>
{% include 'search.njk' %}
{# Footer #}
{% if pageFooter %}
{% include pageFooter %}
{% endif %}
</wa-page>
</body>

View File

@@ -98,6 +98,7 @@
<li><a href="/docs/components/icon/">Icon</a></li>
<li><a href="/docs/components/include/">Include</a></li>
<li><a href="/docs/components/input/">Input</a></li>
<li><a href="/docs/components/intersection-observer">Intersection Observer</a></li>
<li><a href="/docs/components/mutation-observer/">Mutation Observer</a></li>
<li><a href="/docs/components/popover/">Popover</a></li>
<li><a href="/docs/components/popup/">Popup</a></li>

View File

@@ -504,7 +504,6 @@ wa-card .page-name {
width: 100%;
height: 100%;
display: block;
--background-color-hover: transparent;
font-family: var(--wa-font-family-code);
&::part(button) {
@@ -514,6 +513,7 @@ wa-card .page-name {
}
&::part(button):hover {
background-color: transparent;
cursor: copy;
}

View File

@@ -0,0 +1,297 @@
---
title: Intersection Observer
description: Tracks immediate child elements and fires events as they move in and out of view.
layout: component
---
This component leverages the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) to track when its direct children enter or leave a designated root element. The `wa-intersect` event fires whenever elements cross the visibility threshold.
```html {.example}
<div id="intersection__overview">
<wa-intersection-observer threshold="1" intersect-class="visible">
<div class="box"><wa-icon name="bulb"></wa-icon></div>
</wa-intersection-observer>
</div>
<small>Scroll to see the element intersect at 100% visibility</small>
<style>
/* Container styles */
#intersection__overview {
display: flex;
flex-direction: column;
gap: 2rem;
height: 300px;
border: solid 2px var(--wa-color-surface-border);
padding: 1rem;
overflow-y: auto;
/* Spacers to demonstrate scrolling */
&::before {
content: '';
height: 260px;
flex-shrink: 0;
}
&::after {
content: '';
height: 260px;
flex-shrink: 0;
}
/* Box styles */
.box {
flex-shrink: 0;
width: 120px;
height: 120px;
background-color: var(--wa-color-neutral-fill-normal);
color: var(--wa-color-neutral-10);
display: flex;
align-items: center;
justify-content: center;
margin-inline: auto;
transition: all 50ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
wa-icon {
font-size: 3rem;
stroke-width: 1px;
}
&.visible {
background-color: var(--wa-color-brand-60);
color: white;
}
}
+ small {
display: block;
text-align: center;
margin-block-start: 1rem;
}
}
</style>
```
:::info
Keep in mind that only direct children of the host element are monitored. Nested elements won't trigger intersection events.
:::
## Usage Examples
### Adding Observable Content
The intersection observer tracks only its direct children. The component uses [`display: contents`](https://developer.mozilla.org/en-US/docs/Web/CSS/display#contents) styling, which makes it seamless to integrate with flex and grid layouts from a parent container.
```html
<div style="display: flex; flex-direction: column;">
<wa-intersection-observer>
<div class="box">Box 1</div>
<div class="box">Box 2</div>
<div class="box">Box 3</div>
</wa-intersection-observer>
</div>
```
The component tracks elements as they enter and exit the root element (viewport by default) and emits the `wa-intersect` event on state changes. The event provides `event.detail.entry`, an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) object with intersection details.
You can identify the triggering element through `entry.target`. Check `entry.isIntersecting` to determine if an element is entering or exiting the viewport.
```javascript
observer.addEventListener('wa-intersect', event => {
const entry = event.detail.entry;
if (entry.isIntersecting) {
console.log('Element entered viewport:', entry.target);
} else {
console.log('Element left viewport:', entry.target);
}
});
```
### Setting a Custom Root Element
You can observe intersections within a specific container by assigning the `root` attribute to the [root element's](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) ID. Apply [`rootMargin`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) with the `root-margin` attribute to expand or contract the observation area.
```html
<div id="scroll-container">
<wa-intersection-observer root="scroll-container" root-margin="50px 0px"> ... </wa-intersection-observer>
</div>
```
### Configuring Multiple Thresholds
Track different visibility percentages by providing multiple [`threshold`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold) values as a space-separated list.
```html
<wa-intersection-observer threshold="0 0.25 0.5 0.75 1"> ... </wa-intersection-observer>
```
### Applying Classes on Intersect
The `intersect-class` attribute automatically toggles the specified class on direct children when they become visible. This enables pure CSS styling without JavaScript event handlers.
```html {.example}
<div id="intersection__classes">
<wa-intersection-observer threshold="0.5" intersect-class="visible" root="intersection__classes">
<div class="box fade">Fade In</div>
<div class="box slide">Slide In</div>
<div class="box scale">Scale & Rotate</div>
<div class="box bounce">Bounce</div>
</wa-intersection-observer>
</div>
<small>Scroll to see elements transition at 50% visibility</small>
<style>
/* Container styles */
#intersection__classes {
display: flex;
flex-direction: column;
gap: 2rem;
height: 300px;
border: solid 2px var(--wa-color-surface-border);
padding: 1rem;
overflow-y: auto;
/* Spacers to demonstrate scrolling */
&::before {
content: '';
height: 260px;
flex-shrink: 0;
}
&::after {
content: '';
height: 260px;
flex-shrink: 0;
}
+ small {
display: block;
text-align: center;
margin-block-start: 1rem;
}
/* Shared box styles */
.box {
flex-shrink: 0;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: white;
opacity: 0;
padding: 2rem;
margin-inline: auto;
/* Fade */
&.fade {
background: var(--wa-color-brand-fill-loud);
color: var(--wa-color-brand-on-loud);
transform: translateY(30px);
transition: all 0.6s ease;
&.visible {
opacity: 1;
transform: translateY(0);
}
}
/* Slide */
&.slide {
background: var(--wa-color-brand-fill-loud);
color: var(--wa-color-brand-on-loud);
transform: translateX(-50px);
transition: all 0.5s ease;
&.visible {
opacity: 1;
transform: translateX(0);
}
}
/* Scale */
&.scale {
background: var(--wa-color-brand-fill-loud);
color: var(--wa-color-brand-on-loud);
transform: scale(0.6) rotate(-15deg);
transition: all 0.7s cubic-bezier(0.175, 0.885, 0.32, 1.275);
&.visible {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
/* Bounce In and Out */
&.bounce {
background: var(--wa-color-brand-fill-loud);
color: var(--wa-color-brand-on-loud);
opacity: 0;
transform: scale(0.8);
transition: none;
&.visible {
opacity: 1;
transform: scale(1);
animation: bounceIn 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}
&:not(.visible) {
animation: bounceOut 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}
}
}
}
@keyframes bounceIn {
0% {
transform: scale(0.8);
}
40% {
transform: scale(1.08);
}
65% {
transform: scale(0.98);
}
80% {
transform: scale(1.02);
}
90% {
transform: scale(0.99);
}
100% {
transform: scale(1);
}
}
@keyframes bounceOut {
0% {
transform: scale(1);
opacity: 1;
}
20% {
transform: scale(1.02);
opacity: 1;
}
40% {
transform: scale(0.98);
opacity: 0.8;
}
60% {
transform: scale(1.05);
opacity: 0.6;
}
80% {
transform: scale(0.95);
opacity: 0.3;
}
100% {
transform: scale(0.8);
opacity: 0;
}
}
</style>
```

View File

@@ -18,6 +18,7 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
- Removed the `fixed-width` attribute as it's now the default behavior
- 🚨 BREAKING: Renamed the `icon-position` attribute to `icon-placement` in `<wa-details>` [discuss:1340]
- 🚨 BREAKING: Removed the `size` attribute from `<wa-button-group>` as it only set the initial size and gets out of sync when buttons are updated (apply a `size` to each button instead)
- Added the `<wa-intersection-observer>` component
- Added the Hindi translation [pr:1307]
- Added `--show-duration` and `--hide-duration` to `<wa-select>` [issue:1281]
- Fixed incorrectly named exported tooltip parts in `<wa-slider>` [pr:1277]
@@ -33,6 +34,8 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
- Fixed spacing in `<wa-input>` when both clear and password toggle icons are present [issue:1325]
- Fixed a bug in `<wa-radio-group>` and `<wa-radio>` where changing appearances dynamically would render incorrectly [issue:1178]
- Fixed a bug in `<wa-input>` that prevented the value from changing when assigning non-string values to `value` [issue:1323]
- Fixed a bug in `<wa-color-picker>` that prevent the picker from staying in the viewport
- Fixed a bug that in `<wa-icon>` that caused `library`, `family`, `variant` and `name` to not reflect [pr:#1395]
- Added horizontal orientation support with `orientation="horizontal"` for `<wa-card>`
## 3.0.0-beta.4

View File

@@ -59,7 +59,6 @@ Color is organized by three main categories:
- [Foundational colors](#foundational-colors) that lay the groundwork for your theme
- [Semantic colors](#semantic-colors) that draw attention and convey meaning
## Color Scales
Color scales are determined by your [color palette](/docs/color-palettes) and are made up of the lowest level color tokens in your theme. Each token is identified by a name, like red or gray, and numerical tint based on the color's lightness. On this scale, 100 is equal to pure white and 0 is equal to pure black.
@@ -73,6 +72,7 @@ You can use these tints to ensure accessible color contrast per [WCAG 2.1 succes
You have several hand-crafted [color palettes](/docs/color-palettes) to choose from. Each palette defines 10 hues each with a scale of 11 tints using the format `--wa-color-{hue}-{tint}`.
{% for hue in ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'] -%}
<div class="color-name">{{ hue | capitalize }}</div>
<ul class="color-group">
{% for tint in ['95', '90', '80', '70', '60', '50', '40', '30', '20', '10', '05'] -%}
@@ -91,6 +91,7 @@ You have several hand-crafted [color palettes](/docs/color-palettes) to choose f
Any hue can be mapped to `brand`, `neutral`, `success`, `warning`, and `danger` scales. Like the tokens in a color scale, each token is identified by its semantic group and a numerical tint using the format `--wa-color-{group}-{tint}`.
{% for group in ['brand', 'neutral', 'success', 'warning', 'danger'] -%}
<div class="color-name">{{ group | capitalize }}</div>
<ul class="color-group">
{% for tint in ['95', '90', '80', '70', '60', '50', '40', '30', '20', '10', '05'] -%}
@@ -112,19 +113,19 @@ Foundational colors lay the groundwork for the content and structure of your pro
Surfaces are background layers that other content rests on. Surface colors help convey hierarchy through a sense of elevation, where `--wa-color-surface-raised` is the closest to the user (e.g., dialogs and popup menus) and `--wa-color-surface-lowered` is the farthest away (e.g., wells).
| Custom Property | Preview |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `--wa-color-surface-raised` | <div class="swatch" style="background-color: var(--wa-color-surface-raised); box-shadow:var(--wa-shadow-s)"></div> |
| `--wa-color-surface-default` | <div class="swatch" style="background-color: var(--wa-color-surface-default)"></div> |
| `--wa-color-surface-lowered` | <div class="swatch" style="background-color: var(--wa-color-surface-lowered); box-shadow: inset var(--wa-shadow-s)"></div> |
| `--wa-color-surface-border` | <div class="swatch" style="border-color: var(--wa-color-surface-border)"></div> |
| Custom Property | Preview |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `--wa-color-surface-raised` | <div class="swatch" style="background-color: var(--wa-color-surface-raised); box-shadow:var(--wa-shadow-s)"></div> |
| `--wa-color-surface-default` | <div class="swatch" style="background-color: var(--wa-color-surface-default)"></div> |
| `--wa-color-surface-lowered` | <div class="swatch" style="background-color: var(--wa-color-surface-lowered); box-shadow: inset var(--wa-shadow-s)"></div> |
| `--wa-color-surface-border` | <div class="swatch" style="border-color: var(--wa-color-surface-border)"></div> |
### Text
Text colors are used for standard text elements. We recommend a minimum 4.5:1 contrast ratio between text colors and surface colors.
| Custom Property | Preview |
| ------------------------ | ---------------------------------------------------------- |
| Custom Property | Preview |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `--wa-color-text-normal` | <div class="swatch" value="--wa-color-text-normal" style="color: var(--wa-color-text-normal); display: inline-block;">AaBb</div> |
| `--wa-color-text-quiet` | <div class="swatch" value="--wa-color-text-normal" style="color: var(--wa-color-text-quiet); display: inline-block;">AaBb</div> |
| `--wa-color-text-link` | <div class="swatch" value="--wa-color-text-normal" style="color: var(--wa-color-text-link); display: inline-block;">AaBb</div> |
@@ -153,23 +154,23 @@ This is used alongside other [shadow tokens](/docs/tokens/shadows) to construct
Web Awesome uses a single focus color for predictable keyboard navigation. This is used alongside other [focus tokens](/docs/tokens/focus) to construct `--wa-focus-ring`. We recommend a minimum 3:1 contrast ratio against surface colors and background colors wherever possible.
| Custom Property | Preview |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Custom Property | Preview |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--wa-color-focus` | <div class="swatch" value="--wa-color-focus" style="outline: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-color-focus)"></div> |
#### Hover and Active
Web Awesome leverages `color-mix()` to achieve consistent hover and active states across components without the need for untold numbers of handpicked colors. Through `color-mix()`, these custom properties contextually generate hover and active colors based on the color of the component.
| Custom Property | Preview |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `--wa-color-mix-hover` | <div class="swatch color-mix-example" value="--wa-color-mix-hover" style="--mix-color: var(--wa-color-mix-hover)"><small>mixed</small></div> |
| Custom Property | Preview |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `--wa-color-mix-hover` | <div class="swatch color-mix-example" value="--wa-color-mix-hover" style="--mix-color: var(--wa-color-mix-hover)"><small>mixed</small></div> |
| `--wa-color-mix-active` | <div class="swatch color-mix-example" value="--wa-color-mix-active" style="--mix-color: var(--wa-color-mix-active)"><small>mixed</small></div> |
## Semantic Colors
Semantic colors reinforce a specific message, intended usage, or expected results through familiar, meaningful hues. Each color is identified by its semantic group, role, and attention using the format `--wa-color-{group}-{role}-{attention}`. There are five groups of semantic colors:
- **Brand** to emphasize your brand color
- **Success** for validity or confirmation
- **Neutral** for ordinary or inactive content
@@ -177,16 +178,19 @@ Semantic colors reinforce a specific message, intended usage, or expected result
- **Danger** for errors or risk
Each group defines colors for specific roles so that colors can be easily assembled with predictable results and readable contrast. There are three roles:
- **Fill** for background colors or areas larger than a few pixels
- **Border** for borders, dividers, and other stroke-width elements
- **On** for content displayed on a fill (e.g., pair `--wa-color-danger-on-loud` with `--wa-color-danger-fill-loud`)
Finally, each color is named according to how much attention it draws. Here, we use noise as an analogy: a loud noise draws more attention than a quiet one. There are three levels of attention:
- **Quiet** draws the least attention
- **Normal** draws some attention
- **Loud** draws the most attention
{% set variants = ['brand', 'success', 'neutral', 'warning', 'danger'] %}
<table>
<thead>
<tr>

View File

@@ -1327,6 +1327,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
distance="0"
skidding="0"
sync="width"
flip
flip-fallback-strategy="best-fit"
shift
shift-padding="10"
aria-disabled=${this.disabled ? 'true' : 'false'}
@wa-after-show=${this.handleAfterShow}
@wa-after-hide=${this.handleAfterHide}

View File

@@ -46,7 +46,7 @@ export default class WaIcon extends WebAwesomeElement {
@state() private svg: SVGElement | HTMLTemplateResult | null = null;
/** The name of the icon to draw. Available names depend on the icon library being used. */
@property() name?: string;
@property({ reflect: true }) name?: string;
/**
* The family of icons to choose from. For Font Awesome Free, valid options include `classic` and `brands`. For
@@ -54,14 +54,14 @@ export default class WaIcon extends WebAwesomeElement {
* A valid kit code must be present to show pro icons via CDN. You can set `<html data-fa-kit-code="...">` to provide
* one.
*/
@property() family: string;
@property({ reflect: true }) family: string;
/**
* The name of the icon's variant. For Font Awesome, valid options include `thin`, `light`, `regular`, and `solid` for
* the `classic` and `sharp` families. Some variants require a Font Awesome Pro subscription. Custom icon libraries
* may or may not use this property.
*/
@property() variant: string;
@property({ reflect: true }) variant: string;
/** Sets the width of the icon to match the cropped SVG viewBox. This operates like the Font `fa-width-auto` class. */
@property({ attribute: 'auto-width', type: Boolean, reflect: true }) autoWidth: false;
@@ -82,7 +82,7 @@ export default class WaIcon extends WebAwesomeElement {
@property() label = '';
/** The name of a registered custom icon library. */
@property() library = 'default';
@property({ reflect: true }) library = 'default';
connectedCallback() {
super.connectedCallback();

View File

@@ -1,7 +1,7 @@
import { getKitCode } from '../../utilities/base-path.js';
import type { IconLibrary } from './library.js';
const FA_VERSION = '7.0.0';
const FA_VERSION = '7.0.1';
function getIconUrl(name: string, family: string, variant: string) {
const kitCode = getKitCode();

View File

@@ -0,0 +1,3 @@
:host {
display: contents;
}

View File

@@ -0,0 +1,9 @@
import { expect, fixture, html } from '@open-wc/testing';
describe('<wa-intersection-observer>', () => {
it('should render a component', async () => {
const el = await fixture(html` <wa-intersection-observer></wa-intersection-observer> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,200 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { WaIntersectEvent } from '../../events/intersect.js';
import { clamp } from '../../internal/math.js';
import { parseSpaceDelimitedTokens } from '../../internal/parse.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './intersection-observer.css';
/**
* @summary Tracks immediate child elements and fires events as they move in and out of view.
* @documentation https://webawesome.com/docs/components/intersection-observer
* @status stable
* @since 2.0
*
* @slot - Elements to track. Only immediate children of the host are monitored.
*
* @event {{ entry: IntersectionObserverEntry }} wa-intersect - Fired when a tracked element begins or ceases intersecting.
*/
@customElement('wa-intersection-observer')
export default class WaIntersectionObserver extends WebAwesomeElement {
static css = styles;
private intersectionObserver: IntersectionObserver | null = null;
private observedElements = new Map<Element, boolean>();
/** Element ID to define the viewport boundaries for tracked targets. */
@property() root: string | null = null;
/** Offset space around the root boundary. Accepts values like CSS margin syntax. */
@property({ attribute: 'root-margin' }) rootMargin = '0px';
/** One or more space-separated values representing visibility percentages that trigger the observer callback. */
@property() threshold = '0';
/**
* CSS class applied to elements during intersection. Automatically removed when elements leave
* the viewport, enabling pure CSS styling based on visibility state.
*/
@property({ attribute: 'intersect-class' }) intersectClass = '';
/** If enabled, observation ceases after initial intersection. */
@property({ type: Boolean, reflect: true }) once = false;
/** Deactivates the intersection observer functionality. */
@property({ type: Boolean, reflect: true }) disabled = false;
connectedCallback() {
super.connectedCallback();
if (!this.disabled) {
this.updateComplete.then(() => {
this.startObserver();
});
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopObserver();
}
private handleSlotChange() {
if (!this.disabled) {
this.startObserver();
}
}
/** Converts threshold property string into numeric array. */
private parseThreshold(): number[] {
const tokens = parseSpaceDelimitedTokens(this.threshold);
return tokens.map((token: string) => {
const num = parseFloat(token);
return isNaN(num) ? 0 : clamp(num, 0, 1);
});
}
/** Locates and returns the root element using the specified ID. */
private resolveRoot(): Element | null {
if (!this.root) return null;
try {
const doc = this.getRootNode() as Document | ShadowRoot;
const target = doc.getElementById(this.root);
if (!target) {
console.warn(`Root element with ID "${this.root}" could not be found.`, this);
}
return target;
} catch {
console.warn(`Invalid selector for root: "${this.root}"`, this);
return null;
}
}
/** Initializes or reinitializes the intersection observer instance. */
private startObserver() {
this.stopObserver();
// Skip setup if functionality is disabled
if (this.disabled) return;
// Convert threshold string to numeric values
const threshold = this.parseThreshold();
// Locate the root boundary element
const rootElement = this.resolveRoot();
// Set up unified observer for all child elements
this.intersectionObserver = new IntersectionObserver(
entries => {
entries.forEach(entry => {
const wasIntersecting = this.observedElements.get(entry.target) ?? false;
const isIntersecting = entry.isIntersecting;
// Record current intersection state
this.observedElements.set(entry.target, isIntersecting);
// Toggle intersection class based on visibility
if (this.intersectClass) {
if (isIntersecting) {
entry.target.classList.add(this.intersectClass);
} else {
entry.target.classList.remove(this.intersectClass);
}
}
// Emit the intersection event
const changeEvent = new WaIntersectEvent({ entry });
this.dispatchEvent(changeEvent);
if (isIntersecting && !wasIntersecting) {
// When once mode is active, cease tracking after first intersection
if (this.once) {
this.intersectionObserver?.unobserve(entry.target);
this.observedElements.delete(entry.target);
}
}
});
},
{
root: rootElement,
rootMargin: this.rootMargin,
threshold,
},
);
// Begin tracking all immediate child elements
const slot = this.shadowRoot!.querySelector('slot');
if (slot !== null) {
const elements = slot.assignedElements({ flatten: true });
elements.forEach(element => {
this.intersectionObserver?.observe(element);
// Set initial non-intersecting state
this.observedElements.set(element, false);
});
}
}
/** Halts the intersection observer and cleans up. */
private stopObserver() {
// Clear intersection classes from all tracked elements before stopping
if (this.intersectClass) {
this.observedElements.forEach((_, element) => {
element.classList.remove(this.intersectClass);
});
}
this.intersectionObserver?.disconnect();
this.intersectionObserver = null;
this.observedElements.clear();
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
if (this.disabled) {
this.stopObserver();
} else {
this.startObserver();
}
}
@watch('root', { waitUntilFirstUpdate: true })
@watch('rootMargin', { waitUntilFirstUpdate: true })
@watch('threshold', { waitUntilFirstUpdate: true })
handleOptionsChange() {
this.startObserver();
}
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'wa-intersection-observer': WaIntersectionObserver;
}
}

View File

@@ -11,6 +11,7 @@ export type { WaExpandEvent } from './expand.js';
export type { WaFinishEvent } from './finish.js';
export type { WaHideEvent } from './hide.js';
export type { WaHoverEvent } from './hover.js';
export type { WaIntersectEvent } from './intersect.js';
export type { WaInvalidEvent } from './invalid.js';
export type { WaLazyChangeEvent } from './lazy-change.js';
export type { WaLazyLoadEvent } from './lazy-load.js';

View File

@@ -0,0 +1,19 @@
/** Emitted when an element's intersection state changes. */
export class WaIntersectEvent extends Event {
readonly detail?: WaIntersectEventDetail;
constructor(detail?: WaIntersectEventDetail) {
super('wa-intersect', { bubbles: false, cancelable: false, composed: true });
this.detail = detail;
}
}
interface WaIntersectEventDetail {
entry?: IntersectionObserverEntry;
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-intersect': WaIntersectEvent;
}
}

View File

@@ -1,5 +1,9 @@
#!/bin/bash
echo "Running the build!"
git clone "https://konnorrogers:$GITHUB_ACCESS_TOKEN@github.com/shoelace-style/webawesome-pro" packages/webawesome-pro
if [[ "$CLONE_PRO" != "false" ]]; then
git clone "https://konnorrogers:$GITHUB_ACCESS_TOKEN@github.com/shoelace-style/webawesome-pro" packages/webawesome-pro
fi
cd packages/webawesome-pro && npm run build