Compare commits

..

2 Commits

Author SHA1 Message Date
Lea Verou
be3b595e99 Preview theme with different palettes 2025-01-14 17:08:06 -05:00
Lea Verou
1ad43b3b7d Theme palette (static) 2025-01-14 16:29:30 -05:00
171 changed files with 3156 additions and 4332 deletions

View File

@@ -1 +0,0 @@
["red", "yellow", "green", "teal", "blue", "indigo", "violet", "gray"]

View File

@@ -1 +0,0 @@
export { default as default } from '../../src/styles/color/palettes.js';

View File

@@ -16,7 +16,7 @@
{# Docs styles #}
<link rel="stylesheet" href="/assets/styles/docs.css" />
</head>
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
<body class="layout-{{ layout | stripExtension }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop" disable-navigation-toggle="">
<header slot="header" class="wa-split">

View File

@@ -1,40 +0,0 @@
<table class="colors wa-palette-{{ paletteId }}">
<thead>
<tr>
<th></th>
{% for tint_bg in tints -%}
{% for tint_fg in tints | reverse -%}
{% if (tint_fg - tint_bg) | abs == difference %}
<th>{{ tint_fg }} on {{ tint_bg }}</th>
{% endif %}
{%- endfor -%}
{%- endfor %}
</tr>
</thead>
{% for hue in hues -%}
<tr>
<th>{{ hue | capitalize }}</th>
{% for tint_bg in tints -%}
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
{% for tint_fg in tints | reverse -%}
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
{% if (tint_fg - tint_bg) | abs == difference %}
<td>
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
{% set contrast_wcag = '' %}
{% if color_fg and color_bg %}
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
{% endif %}
{% if contrast_wcag %}
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
{% else %}
{{ tint_fg }} on {{ tint_bg }}
{% endif %}
</div>
</td>
{% endif %}
{%- endfor -%}
{%- endfor -%}
</tr>
{%- endfor %}
</table>

View File

@@ -1,24 +0,0 @@
{%- if not stylesheets %}{% set stylesheets = [stylesheet] %}{% endif -%}
<wa-tab-group>
<wa-tab panel="html">In HTML</wa-tab>
<wa-tab panel="css">In CSS</wa-tab>
<wa-tab-panel name="html">
Simply add the following code to the `<head>` of your page:
```html
{% for stylesheet in stylesheets -%}
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />{% if not loop.last %}
{% endif %}{% endfor %}
```
</wa-tab-panel>
<wa-tab-panel name="css">
Simply add the following code at the top of your CSS file:
```css
{% for stylesheet in stylesheets -%}
@import url('{% cdnUrl stylesheet %}');{% if not loop.last %}
{% endif %}{% endfor %}
```
</wa-tab-panel>
</wa-tab-group>

View File

@@ -0,0 +1,41 @@
<div class="color-palette">
<template shadowrootmode="open">
<link href="/assets/styles/docs.css" rel="stylesheet" />
<link id="theme" href="/dist/styles/themes/{{ themeId }}.css" rel="stylesheet" />
<link id="palette" href="/dist/styles/color/{{ palette }}.css" rel="stylesheet" />
<ul class="color-group">
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-brand-fill-loud)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-neutral-fill-loud)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-success-fill-loud)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-warning-fill-loud)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-danger-fill-loud)"></div>
</li>
</ul>
<ul class="color-group">
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-brand-fill-normal)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-neutral-fill-normal)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-success-fill-normal)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-warning-fill-normal)"></div>
</li>
<li class="color-preview">
<div class="swatch" style="background-color: var(--wa-color-danger-fill-normal)"></div>
</li>
</ul>
</template>
</div>

View File

@@ -25,7 +25,6 @@
'utilities': 'Style Utilities',
'layout': 'Layout',
'patterns': 'Patterns',
'palettes': 'Color Palettes',
'tokens': 'Design Tokens'
} %}
{% include 'sidebar-group.njk' %}

View File

@@ -1,24 +0,0 @@
{% set paletteId = page.fileSlug %}
{% set tints = [80, 60, 40, 20] %}
{% set width = 20 %}
{% set height = 13 %}
{% set gap_x = 3 %}
{% set gap_y = 3 %}
<svg viewBox="0 0 {{ (width + gap_x) * hues|length }} {{ (height + gap_y) * tints|length }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
<style>
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
.palette-icon {
height: 8ch;
}
</style>
{% for hue in hues -%}
{% set hueIndex = loop.index0 %}
{% for tint in tints -%}
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ loop.index0 * (height + gap_y) }}"
width="{{ width }}" height="{{ height }}"
fill="var(--wa-color-{{ hue }}-{{ tint }})" rx="4" />
{%- endfor %}
{% endfor %}
</svg>

View File

@@ -1,119 +0,0 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{# {% set forceTheme = page.fileSlug %} #}
{% extends '../_layouts/block.njk' %}
{% set paletteId = page.fileSlug %}
{% block afterContent %}
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
<table class="colors wa-palette-{{ paletteId }}">
<thead>
<tr>
<th></th>
{% for tint in tints -%}
<th>{{ tint }}</th>
{%- endfor %}
</tr>
</thead>
{% for hue in hues -%}
<tr>
<th>{{ hue | capitalize }}</th>
{% for tint in tints -%}
<td>
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div>
</td>
{%- endfor -%}
</tr>
{%- endfor %}
</table>
<h2>Used By</h2>
<section class="index-grid">
{% for page in collections.theme %}
{%- if page.data.palette == paletteId -%}
{% include "page-card.njk" %}
{%- endif -%}
{% endfor %}
</section>
{% markdown %}
## Color Contrast
Web Awesome color scales are designed to guarantee certain contrast ratios,
both per [WCAG 2.1 success criteria](https://www.w3.org/TR/WCAG21/#contrast-minimum)
as well as the emergent APCA specification _(planned)_,
so you can ensure that text is both legible to all users, and legally conformant.
### Level 1
A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for large text and icons (AA).
{% endmarkdown %}
{% set difference = 40 %}
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `45`:
{% endmarkdown %}
{% set difference = 45 %}
{% include "contrast-table.njk" %}
{% markdown %}
### Level 2
A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for normal text (AA) and large text (AAA)
{% endmarkdown %}
{% set difference = 50 %}
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `55`:
{% endmarkdown %}
{% set difference = 55 %}
{% include "contrast-table.njk" %}
{% markdown %}
### Level 3
A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all text (AAA)
{% endmarkdown %}
{% set difference = 60 %}
{% include "contrast-table.njk" %}
{% markdown %}
This also goes for a difference of `65`:
{% endmarkdown %}
{% set difference = 65 %}
{% include "contrast-table.njk" %}
{% markdown %}
## How to use this palette
If you are using a Web Awesome theme that uses this palette, it will already be included.
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
{% include 'import-stylesheet-code.md.njk' %}
{% endmarkdown %}
{% endblock %}

View File

@@ -3,37 +3,52 @@
{# {% set forceTheme = page.fileSlug %} #}
{% extends '../_includes/base.njk' %}
{% set themeId = page.fileSlug %}
{% if themeId == 'remixed' -%}
{% set themeId = 'default' %}
{% endif -%}
{% set palette = defaultPalette %}
{% set palettes = ['default', 'classic', 'rudimentary', 'bright', 'anodized', 'elegant', 'vogue'] %}
{% block header %}
<iframe src='/docs/themes/{{ themeId }}/demo.html' id="demo"></iframe>
{% if palette -%}
<h2>Default Color Palette</h2>
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
{% set themePage = page %}
{% set page = paletteURL | getCollectionItemFromUrl %}
<div class="index-grid">
{% include 'page-card.njk' %}
</div>
{% set page = themePage %}
{% endif %}
<iframe id="theme-demo" src='{{ page.url }}demo.html'></iframe>
<p>
<wa-select label="Color palette:" value="{{ defaultPalette }}" id="palette-picker">
{% for p in palettes -%}
<wa-option value="{{ p }}">{{ p | capitalize }}</wa-option>
{%- endfor %}
</wa-select>
<script type="module" src="/docs/themes/palette-picker.js"></script>
</p>
{% include 'palette.njk' %}
<br>
{% endblock %}
{% block afterContent %}
{% markdown %}
{%- if page.fileSlug != 'remixed' %}
## How to use this theme
You can import this theme from the Web Awesome CDN.
{% set stylesheet = 'styles/themes/' + themeId + '.css' %}
{% include 'import-stylesheet-code.md.njk' %}
{% endif %}
<wa-tab-group>
<wa-tab panel="html">In HTML</wa-tab>
<wa-tab panel="css">In CSS</wa-tab>
<wa-tab-panel name="html">
Simply add the following code to the `<head>` of your page:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/' + page.fileSlug + '.css' %}" />
```
</wa-tab-panel>
<wa-tab-panel name="css">
Simply add the following code at the top of your CSS file:
```css
@import url('{% cdnUrl 'styles/themes/' + page.fileSlug + '.css' %}');
```
</wa-tab-panel>
</wa-tab-group>
## Dark mode

View File

@@ -105,23 +105,6 @@ export function deepValue(obj, key) {
return key.reduce((subObj, property) => subObj?.[property], obj);
}
export function number(value, options) {
if (typeof value !== 'number' && isNaN(value)) {
return value;
}
let lang = options?.lang ?? 'en';
if (options?.lang) {
delete options.lang;
}
if (!options || Object.keys(options).length === 0) {
options = { maximumSignificantDigits: 3 };
}
return Number(value).toLocaleString(lang, options);
}
export function isNumeric(value) {
return typeof value === 'number' || (typeof value === 'string' && !isNaN(value));
}

View File

@@ -18,7 +18,7 @@ function updateResults(input) {
}
}
document.documentElement.addEventListener('input', e => {
document.documentElement.addEventListener('wa-input', e => {
if (e.target?.matches('#block-filter wa-input')) {
updateResults(e.target);
}

View File

@@ -47,6 +47,22 @@ const presetTheme = new ThemeAspect({
},
});
/**
* Without this, there's a flash of the incorrect preset theme.
*/
function updateSelectionBeforeTurboLoad(e) {
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
if (newElement) {
presetTheme.syncUI(newElement);
}
}
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
document.addEventListener(eventName, updateSelectionBeforeTurboLoad);
});
window.addEventListener('turbo:render', e => {
presetTheme.applyChange({ behavior: 'instant' });
});
window.presetTheme = presetTheme;

View File

@@ -28,21 +28,12 @@ export class ThemeAspect {
});
// Listen for selections
document.addEventListener('change', event => {
document.addEventListener('wa-change', event => {
const picker = event.target.closest(this.picker);
if (picker) {
this.set(picker.value);
}
});
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
document.addEventListener(eventName, e => {
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
if (newElement) {
this.syncUI(newElement);
}
});
});
}
get() {

View File

@@ -333,7 +333,6 @@ wa-page > main:has(> .index-grid) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
gap: var(--wa-space-2xl);
margin-block-end: var(--wa-space-3xl);
a {
border-radius: var(--wa-border-radius-l);
@@ -384,23 +383,16 @@ wa-page > main:has(> .index-grid) {
/* Swatches */
.swatch {
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
border-color: var(--wa-color-neutral-border-normal);
/* border-color: var(--wa-color-neutral-border-normal); */
border-color: transparent;
border-style: var(--wa-border-style);
border-width: var(--wa-border-width-s);
border-radius: var(--wa-border-radius-m);
box-sizing: border-box;
min-height: 2.5em;
padding: var(--wa-space-xs);
font-weight: var(--wa-font-weight-semibold);
line-height: var(--wa-line-height-condensed);
&.color {
border-color: transparent;
}
line-height: 2.5;
height: 2.5em;
padding-inline: var(--wa-space-xs);
wa-copy-button {
position: absolute;
@@ -430,32 +422,33 @@ wa-page > main:has(> .index-grid) {
}
}
table.colors {
thead {
th {
text-align: center;
padding-block: 0;
}
}
tbody {
tr {
border: none;
&:hover {
background: transparent;
}
}
.color-name {
font-weight: var(--wa-font-weight-semibold);
margin-block-end: var(--wa-space-2xs);
}
th {
width: 0;
vertical-align: middle;
text-align: right;
}
.color-group {
align-items: start;
display: flex;
flex-wrap: nowrap;
gap: 0.25em;
td {
padding-inline: var(--wa-space-3xs);
padding-block: var(--wa-space-s);
}
&:where(ul) {
list-style: none;
margin: 0;
padding: 0;
}
+ * {
margin-block-start: var(--wa-space-xl);
}
+ & {
margin-block-start: var(--wa-space-2xs);
}
}
.color-preview {
flex: 1 1 auto;
}
/* Layout Examples */
@@ -521,7 +514,7 @@ table.colors {
}
}
.page-wide {
.layout-theme {
wa-page > main {
max-width: 140ch;
@@ -529,9 +522,7 @@ table.colors {
max-width: 80ch;
}
}
}
.layout-theme {
iframe {
width: 100%;
min-height: 16lh;

View File

@@ -77,9 +77,9 @@ This example demonstrates all of the baked-in animations and easings. Animations
easingName.appendChild(option);
});
animationName.addEventListener('change', () => (animation.name = animationName.value));
easingName.addEventListener('change', () => (animation.easing = easingName.value));
playbackRate.addEventListener('input', () => (animation.playbackRate = playbackRate.value));
animationName.addEventListener('wa-change', () => (animation.name = animationName.value));
easingName.addEventListener('wa-change', () => (animation.easing = easingName.value));
playbackRate.addEventListener('wa-input', () => (animation.playbackRate = playbackRate.value));
</script>
<style>

View File

@@ -249,7 +249,7 @@ This example is best demonstrated using a mouse. Try clicking and dragging the s
const carousel = container.querySelector('wa-carousel');
const toggle = container.querySelector('wa-switch');
toggle.addEventListener('change', () => {
toggle.addEventListener('wa-change', () => {
carousel.toggleAttribute('mouse-dragging', toggle.checked);
});
</script>
@@ -450,7 +450,7 @@ Use the `--aspect-ratio` custom property to customize the size of the carousel's
const carousel = document.querySelector('wa-carousel.aspect-ratio');
const aspectRatio = document.querySelector('wa-select[name="aspect"]');
aspectRatio.addEventlistener('change', () => {
aspectRatio.addEventListener('wa-change', () => {
carousel.style.setProperty('--aspect-ratio', aspectRatio.value);
});
})();

View File

@@ -73,4 +73,4 @@ if (name_search.value) {
filterByName(name_search.value);
}
name_search_group.addEventListener('input', e => filterByName(name_search.value));
name_search_group.addEventListener('wa-input', e => filterByName(name_search.value));

View File

@@ -82,7 +82,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
});
// Update validity on change
checkbox.addEventlistener('change', () => {
checkbox.addEventListener('wa-change', () => {
checkbox.setCustomValidity(checkbox.checked ? '' : errorMessage);
});

View File

@@ -16,7 +16,7 @@ icon: format-bytes
const formatter = container.querySelector('wa-format-bytes');
const input = container.querySelector('wa-input');
input.addEventListener('input', () => (formatter.value = input.value || 0));
input.addEventListener('wa-input', () => (formatter.value = input.value || 0));
</script>
```

View File

@@ -19,7 +19,7 @@ Localization is handled by the browser's [`Intl.NumberFormat` API](https://devel
const formatter = container.querySelector('wa-format-number');
const input = container.querySelector('wa-input');
input.addEventListener('input', () => (formatter.value = input.value || 0));
input.addEventListener('wa-input', () => (formatter.value = input.value || 0));
</script>
```

View File

@@ -54,11 +54,11 @@ Popup is a low-level utility built specifically for positioning elements. Do not
const active = container.querySelector('wa-switch[name="active"]');
const arrow = container.querySelector('wa-switch[name="arrow"]');
select.addEventListener('change', () => (popup.placement = select.value));
distance.addEventListener('input', () => (popup.distance = distance.value));
skidding.addEventListener('input', () => (popup.skidding = skidding.value));
active.addEventListener('change', () => (popup.active = active.checked));
arrow.addEventListener('change', () => (popup.arrow = arrow.checked));
select.addEventListener('wa-change', () => (popup.placement = select.value));
distance.addEventListener('wa-input', () => (popup.distance = distance.value));
skidding.addEventListener('wa-input', () => (popup.skidding = skidding.value));
active.addEventListener('wa-change', () => (popup.active = active.checked));
arrow.addEventListener('wa-change', () => (popup.arrow = arrow.checked));
</script>
<style>
@@ -145,7 +145,7 @@ Popups are inactive and hidden until the `active` attribute is applied. Removing
const popup = container.querySelector('wa-popup');
const active = container.querySelector('wa-switch');
active.addEventListener('change', () => (popup.active = active.checked));
active.addEventListener('wa-change', () => (popup.active = active.checked));
</script>
```
@@ -233,7 +233,7 @@ Since placement is preferred when using `flip`, you can observe the popup's curr
const popup = container.querySelector('wa-popup');
const select = container.querySelector('wa-select');
select.addEventListener('change', () => (popup.placement = select.value));
select.addEventListener('wa-change', () => (popup.placement = select.value));
</script>
```
@@ -277,7 +277,7 @@ Use the `distance` attribute to change the distance between the popup and its an
const popup = container.querySelector('wa-popup');
const distance = container.querySelector('wa-slider');
distance.addEventListener('input', () => (popup.distance = distance.value));
distance.addEventListener('wa-input', () => (popup.distance = distance.value));
</script>
```
@@ -321,7 +321,7 @@ The `skidding` attribute is similar to `distance`, but instead allows you to off
const popup = container.querySelector('wa-popup');
const skidding = container.querySelector('wa-slider');
skidding.addEventListener('input', () => (popup.skidding = skidding.value));
skidding.addEventListener('wa-input', () => (popup.skidding = skidding.value));
</script>
```
@@ -409,9 +409,9 @@ By default, the arrow will be aligned as close to the center of the _anchor_ as
const arrowPlacement = container.querySelector('[name="arrow-placement"]');
const arrow = container.querySelector('[name="arrow"]');
placement.addEventListener('change', () => (popup.placement = placement.value));
arrowPlacement.addEventListener('change', () => (popup.arrowPlacement = arrowPlacement.value));
arrow.addEventListener('change', () => (popup.arrow = arrow.checked));
placement.addEventListener('wa-change', () => (popup.placement = placement.value));
arrowPlacement.addEventListener('wa-change', () => (popup.arrowPlacement = arrowPlacement.value));
arrow.addEventListener('wa-change', () => (popup.arrow = arrow.checked));
</script>
</div>
```
@@ -464,7 +464,7 @@ Use the `sync` attribute to make the popup the same width or height as the ancho
const fixed = container.querySelector('wa-switch');
const sync = container.querySelector('wa-select');
sync.addEventListener('change', () => (popup.sync = sync.value));
sync.addEventListener('wa-change', () => (popup.sync = sync.value));
</script>
```
@@ -523,7 +523,7 @@ Toggle the switch and scroll the container to see the difference.
const popup = container.querySelector('wa-popup');
const fixed = container.querySelector('wa-switch');
fixed.addEventListener('change', () => (popup.strategy = fixed.checked ? 'fixed' : 'absolute'));
fixed.addEventListener('wa-change', () => (popup.strategy = fixed.checked ? 'fixed' : 'absolute'));
</script>
```
@@ -575,7 +575,7 @@ Scroll the container to see how the popup flips to prevent clipping.
const popup = container.querySelector('wa-popup');
const flip = container.querySelector('wa-switch');
flip.addEventListener('change', () => (popup.flip = flip.checked));
flip.addEventListener('wa-change', () => (popup.flip = flip.checked));
</script>
```
@@ -670,7 +670,7 @@ Toggle the switch to see the difference.
const popup = container.querySelector('wa-popup');
const shift = container.querySelector('wa-switch');
shift.addEventListener('change', () => (popup.shift = shift.checked));
shift.addEventListener('wa-change', () => (popup.shift = shift.checked));
</script>
```
@@ -731,7 +731,7 @@ Scroll the container to see the popup resize as its available space changes.
const popup = container.querySelector('wa-popup');
const autoSize = container.querySelector('wa-switch');
autoSize.addEventListener('change', () => (popup.autoSize = autoSize.checked ? 'both' : ''));
autoSize.addEventListener('wa-change', () => (popup.autoSize = autoSize.checked ? 'both' : ''));
</script>
```
@@ -782,9 +782,9 @@ When a gap exists between the anchor and the popup element, this option will add
const hoverBridge = container.querySelector('wa-switch');
const distance = container.querySelector('wa-slider[label="Distance"]');
const skidding = container.querySelector('wa-slider[label="Skidding"]');
distance.addEventListener('input', () => (popup.distance = distance.value));
skidding.addEventListener('input', () => (popup.skidding = skidding.value));
hoverBridge.addEventListener('change', () => (popup.hoverBridge = hoverBridge.checked));
distance.addEventListener('wa-input', () => (popup.distance = distance.value));
skidding.addEventListener('wa-input', () => (popup.skidding = skidding.value));
hoverBridge.addEventListener('wa-change', () => (popup.hoverBridge = hoverBridge.checked));
</script>
```
@@ -837,7 +837,7 @@ This example anchors a popup to the mouse cursor using a virtual element. As suc
};
// Only activate the popup when the switch is checked
enabled.addEventListener('change', () => {
enabled.addEventListener('wa-change', () => {
popup.active = enabled.checked;
});

View File

@@ -24,7 +24,7 @@ QR codes are useful for providing small pieces of information to users who can q
customElements.whenDefined('wa-qr-code').then(() => {
input.value = qrCode.value;
input.addEventListener('input', () => (qrCode.value = input.value));
input.addEventListener('wa-input', () => (qrCode.value = input.value));
});
</script>

View File

@@ -65,7 +65,7 @@ The size of [Radios](/docs/components/radio) and [Radio Buttons](/docs/component
<script>
const radioGroup = document.querySelector('.radio-group-size');
radioGroup.addEventlistener('change', () => {
radioGroup.addEventListener('wa-change', () => {
radioGroup.size = radioGroup.value;
});
</script>
@@ -127,7 +127,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
});
// Update validity when a selection is made
form.addEventlistener('change', () => {
form.addEventListener('wa-change', () => {
const isValid = radioGroup.value === '3';
radioGroup.setCustomValidity(isValid ? '' : errorMessage);
});

View File

@@ -420,7 +420,5 @@ This can be hard to conceptualize, so heres a fairly large example showing how l
//
// TODO - remove once we switch to the Popover API
//
customElements.whenDefined('wa-select').then(() => {
document.querySelectorAll('wa-code-demo [slot="preview"] wa-select').forEach(select => select.hoist = true);
});
document.querySelectorAll('wa-code-demo [slot="preview"] wa-select').forEach(select => select.hoist = true);
</script>

View File

@@ -208,7 +208,7 @@ Try resizing the example below with each option and notice how the panels respon
const splitPanel = container.querySelector('wa-split-panel');
const select = container.querySelector('wa-select');
select.addEventlistener('change', () => (splitPanel.primary = select.value));
select.addEventListener('wa-change', () => (splitPanel.primary = select.value));
</script>
```

View File

@@ -88,7 +88,7 @@ The `selection` attribute lets you change the selection behavior of the tree.
const selectionMode = document.querySelector('#selection-mode');
const tree = document.querySelector('.tree-selectable');
selectionMode.addEventlistener('change', () => {
selectionMode.addEventListener('wa-change', () => {
tree.querySelectorAll('wa-tree-item').forEach(item => (item.selected = false));
tree.selection = selectionMode.value;
});

View File

@@ -506,7 +506,7 @@ hasOutline: false
<wa-option data-alpha="remove" value="playful">Playful</wa-option>
<wa-option data-alpha="remove" value="brutalist">Brutalist</wa-option>
<wa-option data-alpha="remove" value="tailspin">Tailspin</wa-option>
<wa-option data-alpha="remove" value="glossy">Glossy</wa-option>
<wa-option data-alpha="remove" value="glassy">Glassy</wa-option>
<wa-option data-alpha="remove" value="active">Active</wa-option>
<wa-option value="classic">Classic</wa-option>
</wa-select>
@@ -803,7 +803,7 @@ hasOutline: false
const queue = [];
let inputTimeout;
variantInput.addEventlistener('change', () => {
variantInput.addEventListener('wa-change', () => {
iconList.dataset.variant = variantInput.value;
});
@@ -823,7 +823,7 @@ hasOutline: false
});
// Filter as the user types
input.addEventListener('input', () => {
input.addEventListener('wa-input', () => {
clearTimeout(inputTimeout);
inputTimeout = setTimeout(() => {
[...iconList.children].map(item => {
@@ -886,7 +886,7 @@ hasOutline: false
case 'active':
colorPalette = 'rudimentary';
break;
case 'glossy':
case 'glassy':
colorPalette = 'elegant';
break;
case 'premium':
@@ -1049,7 +1049,7 @@ hasOutline: false
case 'brutalist':
case 'classic':
case 'awesome':
case 'glossy':
case 'glassy':
case 'active':
assetFolder = themeSelect.value;
break;
@@ -1084,10 +1084,10 @@ hasOutline: false
el.classList.add(`wa-theme-${theme}-${colorMode}`);
}
colorModeSelect.addEventlistener('change', setColorMode);
colorModeSelect.addEventListener('wa-change', setColorMode);
// Theme Switcher
themeSelect.addEventlistener('change', event => {
themeSelect.addEventListener('wa-change', event => {
const theme = event.target.value
const newStylesheet = Object.assign(document.createElement("link"), {
// This media: "print" allows us to lazy load the stylesheet then hot swap it on load.
@@ -1132,14 +1132,14 @@ hasOutline: false
});
// Color Palette
colorSelect.addEventlistener('change', event => {
colorSelect.addEventListener('wa-change', event => {
const colorPalette = event.target.value;
colorStylesheet.href = `/dist/styles/themes/color/${colorPalette}.css`;
});
// Brand Color
brandColor.addEventlistener('change', event => {
brandColor.addEventListener('wa-change', event => {
const documentStyles = document.documentElement.style
documentStyles.setProperty('--wa-color-primary-95', `var(--wa-color-${event.target.value}-95)`);
documentStyles.setProperty('--wa-color-primary-90', `var(--wa-color-${event.target.value}-90)`);
@@ -1223,7 +1223,7 @@ hasOutline: false
})
// Pre-generated logos
logoSelector.addEventlistener('change', event => {
logoSelector.addEventListener('wa-change', event => {
const value = event.currentTarget.value
const projectLogo = previewContainer.querySelector("#project-logo");
@@ -1259,7 +1259,7 @@ hasOutline: false
case 'tailspin':
presetLogoIcons = ['wind', 'feather', 'lemon', 'wind-turbine'];
break;
case 'glossy':
case 'glassy':
presetLogoIcons = ['raindrops', 'citrus-slice', 'lighthouse', 'kiwi-bird'];
break;
case 'active':
@@ -1279,21 +1279,21 @@ hasOutline: false
})
}
themeSelect.addEventlistener('change', setLogoIcons);
themeSelect.addEventListener('wa-change', setLogoIcons);
// Project Name
container.querySelector('[name="project-name"]').addEventListener('input', event => {
container.querySelector('[name="project-name"]').addEventListener('wa-input', event => {
previewContainer.querySelector("#project-name").innerText = event.target.value || event.target.getAttribute("placeholder")
})
// Heading font weight
resetHeadingFontWeightValue()
fontWeightHeading.addEventListener('input', event => {
fontWeightHeading.addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-font-weight-heading', event.target.value);
});
// Heading text
fontFamilyHeading.addEventlistener('change', event => {
fontFamilyHeading.addEventListener('wa-change', event => {
let fontFamily;
switch (event.target.value) {
case 'assistant':
@@ -1351,7 +1351,7 @@ hasOutline: false
})
// Body text
fontFamilyBody.addEventlistener('change', event => {
fontFamilyBody.addEventListener('wa-change', event => {
let fontFamily;
switch (event.target.value) {
case 'assistant':
@@ -1404,7 +1404,7 @@ hasOutline: false
// Body font weight
resetBodyFontWeightValue()
fontWeightBody.addEventListener('input', event => {
fontWeightBody.addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-font-weight-body', event.target.value);
});
@@ -1580,7 +1580,7 @@ hasOutline: false
}
// Swaps icons to the preferred set for the selected theme
themeSelect.addEventlistener('change', event => {
themeSelect.addEventListener('wa-change', event => {
setPreferredIcons();
showIconStyleOptions();
syncLogoIcon();
@@ -1599,32 +1599,32 @@ hasOutline: false
});
// Changes available Icon Styles and swaps icons based on the selected Icon Family
iconFamily.addEventlistener('change', event => {
iconFamily.addEventListener('wa-change', event => {
useFaIcons();
showIconStyleOptions();
});
// Swaps icons based on the selected Icon Style
iconStyle.addEventlistener('change', useFaIcons);
iconStyle.addEventListener('wa-change', useFaIcons);
// Corners
container.querySelector('[name="corners"]').addEventListener('input', event => {
container.querySelector('[name="corners"]').addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-border-radius-scale', `${event.target.value}`);
});
// Border width
container.querySelector('[name="border-width"]').addEventListener('input', event => {
container.querySelector('[name="border-width"]').addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-border-width-scale', `${event.target.value / 16}`);
});
// Border style
borderStyle.addEventListener('input', event => {
borderStyle.addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-border-style', event.target.value);
});
// Spacing style
spacing.addEventListener('input', event => {
spacing.addEventListener('wa-input', event => {
document.documentElement.style.setProperty('--wa-space-scale', `${event.target.value}`);
});

View File

@@ -146,7 +146,7 @@ To create a custom validation error, pass a non-empty string to the `setCustomVa
alert('All fields are valid!');
});
input.addEventListener('input', () => {
input.addEventListener('wa-input', () => {
if (input.value === 'webawesome') {
input.setCustomValidity('');
} else {

View File

@@ -4,7 +4,6 @@ description: Callouts are used to display important messages inline.
component: callout
icon: callout
snippets: '.wa-callout'
noAlpha: true
---
```html {.example}

View File

@@ -33,39 +33,3 @@ Use the [appearance utilities](/docs/utilities/appearance/) to change the select
</select>
</label>
```
### Grouping options
In [modern browsers](https://caniuse.com/mdn-html_elements_select_hr_in_select), you can use the `<hr>` element as a divider:
```html {.example}
<select>
<small>Section 1</small>
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
<hr />
<small>Section 2</small>
<option value="option-4">Option 4</option>
<option value="option-5">Option 5</option>
<option value="option-6">Option 6</option>
</select>
```
To provide labels, you can use the `<optgroup>` element (with or without dividers):
```html {.example}
<select>
<optgroup label="Section 1">
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
<option value="option-3">Option 3</option>
</optgroup>
<optgroup label="Section 2">
<option value="option-4">Option 4</option>
<option value="option-5">Option 5</option>
<hr />
<option value="option-6">Option 6</option>
</optgroup>
</select>
```

View File

@@ -1,5 +0,0 @@
---
title: Anodized
isPro: true
tags: pro
---

View File

@@ -1,3 +0,0 @@
---
title: Bright
---

View File

@@ -1,4 +0,0 @@
---
title: Classic
description: The original Shoelace color palette.
---

View File

@@ -1,4 +0,0 @@
---
title: Default
description: This is the palette used in the default theme.
---

View File

@@ -1,5 +0,0 @@
---
title: Elegant
isPro: true
tags: pro
---

View File

@@ -1,10 +0,0 @@
---
title: Color Palettes
description: Palettes define [literal colors](/docs/tokens/colors) that are used in the design system.
layout: overview
override:tags: []
forTag: palette
categories:
other: Free
pro: Pro
---

View File

@@ -1,5 +0,0 @@
---
title: Mild
isPro: true
tags: pro
---

View File

@@ -1,5 +0,0 @@
---
title: Natural
isPro: true
tags: pro
---

View File

@@ -1,8 +0,0 @@
{
"layout": "palette.njk",
"tags": ["palettes", "palette"],
"eleventyComputed": {
"snippet": ".wa-palette-{{ page.fileSlug }}",
"icon": "palette"
}
}

View File

@@ -1,5 +0,0 @@
---
title: Rudimentary
isPro: true
tags: pro
---

View File

@@ -1,5 +0,0 @@
---
title: Vogue
isPro: true
tags: pro
---

View File

@@ -1,6 +1,5 @@
{
"layout": "block.njk",
"tags": ["patterns"],
"wide": true,
"noAlpha": true
}

View File

@@ -12,29 +12,12 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
:::
## Next
- 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events.
- `wa-input` => `input`
- `wa-change` => `change`
- `wa-blur` => `blur` (this event will no longer bubble, use `focusout` for a bubbling version)
- `wa-focus` => `focus` (this event will no longer bubble)
- Added `.wa-callout` utility class
- Fixed a bug in `<wa-tab-group>` that prevented nested tab groups from working properly
- Fixed slot names for `show-password-icon` and `hide-password-icon` in `<wa-input>` to more intuitively represent their functions
## 3.0.0-alpha.9
- Added `.wa-callout` utility class
- Added new themes:
- Glossy
- Matter
- Premium
- Playful
- Added docs on themes and palettes
- Separated colors and typography out from themes so they can be used independently
- Added test suite to ensure all color palettes provide the color contrast they are supposed to
- Added `.wa-invert` utility class to invert the current color scheme
- Added `:state(blank)` to `<wa-input>`, `<wa-textarea>`, and `<wa-select>` to style form inputs differently when empty.
## 3.0.0-alpha.8

View File

@@ -3,5 +3,5 @@ title: Active
description: Energetic and tactile, always in motion.
isPro: true
tags: pro
palette: rudimentary
defaultPalette: rudimentary
---

View File

@@ -2,5 +2,5 @@
title: Awesome
description: Punchy and vibrant, the rockstar of themes.
order: 0.2
palette: bright
defaultPalette: bright
---

View File

@@ -3,5 +3,5 @@ title: Brutalist
description: Sharp, square, and unapologetically bold.
isPro: true
tags: pro
palette: default
defaultPalette: default
---

View File

@@ -2,5 +2,5 @@
title: Classic
description: Timeless elegance that never goes out of style.
order: 0.1
palette: classic
defaultPalette: classic
---

View File

@@ -2,5 +2,5 @@
title: Default
description: Your trusty companion, like a perfectly broken-in pair of jeans.
order: 0
palette: default
defaultPalette: default
---

View File

@@ -10,19 +10,20 @@ override:tags: []
eleventyComputed:
forceTheme: "{{ theme.fileSlug }}"
---
<script>
document.getElementById('theme-stylesheet').href = '/dist/styles/themes/{{ theme.fileSlug }}.css';
</script>
<script type=module>
document.getElementById('theme-stylesheet').href = '/dist/styles/themes/{{ theme.fileSlug }}.css';
</script>
<link rel="stylesheet" href="/dist/styles/themes/{{ theme.fileSlug }}.css" />
<link id="theme" rel="stylesheet" href="/dist/styles/themes/{{ theme.fileSlug }}.css" />
<link rel="stylesheet" href="/docs/themes/showcase.css" />
{% set content %}
<header>
{% include 'breadcrumbs.njk' %}
<h1 class="title">{{ theme.data.title }}</h1>
<p id="mix_and_match" hidden class="wa-size-s"></p>
<wa-button href="/docs/themes/remixed/?base={{ theme.fileSlug }}" class="remix-link" size="small" variant="brand" target="_parent">
<wa-icon name="merge"></wa-icon>
Remix
</wa-button>
<p>{% include 'status.njk' %}</p>
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
</header>
@@ -37,50 +38,3 @@ eleventyComputed:
{{ content | safe }}
</div>
</wa-image-comparer>
<script>
function updateTheme() {
let params = new URLSearchParams(window.location.search);
let script = document.currentScript;
const stylesheetURLs = {
color: id => `/dist/styles/themes/${ id }/color.css`,
palette: id => `/dist/styles/color/${ id }.css`,
typography: id => `/dist/styles/themes/${ id }/typography.css`
};
const icons = {
color: 'palette',
palette: 'swatchbook',
typography: 'font-case'
}
for (let link of document.querySelectorAll('link.mix-and-match')) {
link.remove();
}
let msgs = [];
for (let name in stylesheetURLs) {
if (params.get(name)) {
let url = stylesheetURLs[name](params.get(name));
script.insertAdjacentHTML('afterend', `<link rel="stylesheet" href="${ url }" class="mix-and-match" />`);
let docsURL = (name === 'palette' ? '/docs/palettes/' : '/docs/themes/') + params.get(name) + '/';
let title = params.get(name).replace(/^[a-z]/g, c => c.toUpperCase());
let icon = icons[name];
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> <a href="${ docsURL }">${ title }</a>`);
}
}
let isRemixed = msgs.length > 0;
document.documentElement.classList.toggle("remixed", isRemixed);
for (let p of mix_and_match) {
p.hidden = !isRemixed;
if (isRemixed) {
let icon =
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong><br> ` + msgs.map(msg => `<wa-badge appearance=outlined>
${ msg }</wa-badge>`).join(' ');
}
}
}
updateTheme();
</script>

8
docs/docs/themes/glassy.md vendored Normal file
View File

@@ -0,0 +1,8 @@
---
title: Glassy
description: Smooth, sleek, and reflective.
isPro: true
tags: pro
noAlpha: true
defaultPalette: elegant
---

View File

@@ -1,7 +0,0 @@
---
title: Glossy
description: Bustling with plenty of luster and shine.
isPro: true
tags: pro
palette: elegant
---

View File

@@ -1,85 +0,0 @@
---
title: Matter
description: Digital design inspired by the real world.
isPro: true
tags: pro
palette: mild
---
Set the page theme to "{{ title }}" from the top right to preview the following examples.
## Floating Labels
This theme implements "floating labels" for `wa-input`, `wa-textarea`, `wa-select`,
which makes labels look like placeholders when the input is empty, does not have an actual placeholder, and is not focused.
```html {.example}
<wa-input label="What is your name?"></wa-input>
<br>
<wa-input label="What is your name?" appearance="filled"></wa-input>
```
## Ripple Effect
This theme implements a ripple effect for buttons, including native buttons.
Click on the following buttons to observe it:
```html {.example}
<wa-button variant="brand">Button</wa-button>
<button class="wa-brand">Button</button>
```
### Customization
You can use several properties to customize the ripple effect.
These properties can be set on any ancestor, including the root element:
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `--wa-ripple-start-radius` | `<length>` | `0.1em` | The starting radius of the ripple effect. |
| `--wa-ripple-start-opacity` | `<number>` | `0.1` | The starting opacity of the ripple effect. |
| `--wa-ripple-duration` | `<time>` | `calc(2 * var(--wa-transition-slow))` | The duration of the ripple effect transition. |
Any of these can be used to disable the ripple effect:
```css
--wa-ripple-start-radius: 0em;
```
```css
--wa-ripple-start-opacity: 0;
```
```css
--wa-ripple-duration: 0s;
```
These properties would only work on the button itself:
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `--wa-ripple-center-x` | `<percentage>` | `50%` | The x-coordinate of the ripple center point as a percentage of the button width. |
| `--wa-ripple-center-y` | `<percentage>` | `50%` | The y-coordinate of the ripple center point as a percentage of the button height. |
### Ripple Center Point
By default the ripple effect starts from the center of the button.
If you want it to start from the position the button was clicked, you can use this JS snippet:
```js
document.addEventListener("mousedown", evt => {
let target = evt.target;
if (!target.matches?.('wa-button, button, .wa-button')) {
return;
}
let rect = target.getBoundingClientRect();
let x = (evt.clientX - rect.left) / rect.width;
let y = (evt.clientY - rect.top) / rect.height;
target.style.setProperty("--mouse-local-x", x);
target.style.setProperty("--mouse-local-y", y);
});
```

View File

@@ -3,5 +3,5 @@ title: Mellow
description: Soft and soothing, like a lazy Sunday morning.
isPro: true
tags: pro
palette: natural
defaultPalette: natural
---

18
docs/docs/themes/palette-picker.js vendored Normal file
View File

@@ -0,0 +1,18 @@
let palettePicker = document.getElementById('palette-picker');
let paletteDisplay = palettePicker.parentNode.nextElementSibling;
let themeDemo = document.getElementById('theme-demo');
palettePicker.addEventListener('wa-change', function () {
let palette = palettePicker.value;
let paletteStylesheet = paletteDisplay.shadowRoot.getElementById('palette');
paletteStylesheet.href = paletteStylesheet.href.replace(/[a-z-]+.css/, palette + '.css');
let demoPaletteStylesheet = themeDemo.contentDocument.getElementById('palette');
let paletteUrl = `/dist/styles/color/${palette}.css`;
if (demoPaletteStylesheet) {
demoPaletteStylesheet.href = paletteUrl;
} else {
let themeStylesheet = themeDemo.contentDocument.getElementById('theme');
themeStylesheet.insertAdjacentHTML('afterend', `<link id="palette" rel="stylesheet" href="${paletteUrl}">`);
}
});

View File

@@ -1,7 +1,8 @@
---
title: Playful
description: Cheerful and engaging, like a playground on screen.
description: Fun, colorful, and full of personality.
isPro: true
tags: pro
palette: rudimentary
noAlpha: true
defaultPalette: rudimentary
---

View File

@@ -3,5 +3,5 @@ title: Premium
description: The ultimate in sophistication and style.
isPro: true
tags: pro
palette: anodized
defaultPalette: anodized
---

View File

@@ -1,138 +0,0 @@
let params = { base: 'default', palette: '', color: '', typography: '' };
// Find usage code snippet and prepare it
let snippets = document.querySelectorAll('#remixed-usage ~ wa-tab-group pre > code');
let copyButtons = document.querySelectorAll('#remixed-usage ~ wa-tab-group pre > wa-copy-button');
let codeExamples = [];
for (let snippet of snippets) {
let tokens = [...snippet.children];
let base = tokens.shift();
let [palette, color, typography] = tokens;
codeExamples.push({ snippet, base, palette, color, typography });
// Remove non-base tokens
for (let token of tokens) {
let whitespace = token.previousSibling;
if (whitespace.nodeType === Node.TEXT_NODE) {
// Move whitespace to beginning of node
token.prepend(token.previousSibling);
}
}
}
// Read URL params and apply them. This facilitates permalinks.
if (location.search) {
let urlParams = new URLSearchParams(location.search);
for (let aspect in params) {
if (urlParams.has(aspect)) {
params[aspect] = urlParams.get(aspect);
}
}
}
const selects = Object.fromEntries(
[...document.querySelectorAll('#mix_and_match wa-select')].map(select => [select.getAttribute('name'), select]),
);
document.querySelector('#mix_and_match').addEventListener(
'change',
function (event) {
for (let name in selects) {
params[name] = selects[name].value;
}
render();
},
{ capture: true },
);
function hasOverride(name) {
if (!params[name]) {
return false;
}
if (name === 'palette') {
return params[name] !== defaultPalettes[params.base];
}
if (name !== 'base') {
return params[name] !== params.base;
}
return true;
}
function render() {
let demoUrl = new URL(`/docs/themes/${params.base}/demo.html`, location);
let pageParams = new URLSearchParams(params);
for (let aspect in params) {
if (aspect !== 'base' && params[aspect]) {
demoUrl.searchParams.set(aspect, params[aspect]);
}
if (!params[aspect] || (aspect === 'base' && params[aspect] === 'default') || !hasOverride(aspect)) {
pageParams.delete(aspect);
}
// Output code snippet
if (aspect !== 'base') {
for (let codeExample of codeExamples) {
let token = codeExample[aspect];
let value = params[aspect];
if (hasOverride(aspect)) {
// Update code example
let valueToken = [...token.querySelectorAll('.code-attr-value, .code-url')].pop();
valueToken.textContent = replaceStyleSheetURL(valueToken.textContent, aspect, value);
// Add code example to <pre>
codeExample.snippet.append(token);
} else {
token.remove();
}
}
}
// Update selects
if (selects[aspect].value === undefined) {
selects[aspect].setAttribute('value', params[aspect]);
} else {
selects[aspect].value = params[aspect];
}
}
// Update code snippet copy buttons
for (let copyButton of copyButtons) {
copyButton.value = copyButton.nextElementSibling.textContent;
}
// Update demo URL
demo.src = demoUrl;
// Update page URL. If theres already a search, replace it.
// We dont want to clog the users history while they iterate
let historyAction = location.search ? 'replaceState' : 'pushState';
history[historyAction](null, '', `?${pageParams}`);
}
const regexes = {
base: /\/themes\/([a-z-]+)\.css/,
palette: /\/color\/([a-z-]+)\.css/,
color: /\/themes\/([a-z-]+)\/color\.css/,
typography: /\/themes\/([a-z-]+)\/typography\.css/,
};
function replaceStyleSheetURL(url, name, value) {
let regex = regexes[name];
return url.replace(regex, (match, oldValue) => {
return match.replace(oldValue, value);
});
}
globalThis.params = params;
render();

View File

@@ -1,84 +0,0 @@
---
title: Remixed
description: TODO
isPro: true
tags: pro
---
<link rel="stylesheet" href="{{ page.url }}/style.css">
{% block header %}
<script>
globalThis.defaultPalettes = {
{% for theme in collections.theme | sort -%}
"{{ theme.fileSlug }}": "{{ theme.data.palette }}",
{%- endfor %}
};
</script>
<p id="mix_and_match" class="wa-gap-m">
{# <strong>
<wa-icon name="merge" slot="prefix"></wa-icon>
Remix
<wa-icon-button href="#remixing" name="circle-question" slot="suffix" variant="regular" label="How to use?"></wa-icon-button>
</strong> #}
<wa-select name="base" label="Base:" size="small" value="default">
<wa-icon name="brush" slot="prefix" variant="regular"></wa-icon>
{% for theme in collections.theme | sort %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}" data-palette="{{ theme.data.palette }}">{{ theme.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
<wa-select name="palette" label="Palette" size="small">
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for p in collections.palette | sort %}
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
{% endfor %}
</wa-select>
<wa-select name="color" label="Colors from…" size="small" value="">
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" size="small" value="">
<wa-icon name="font-case" slot="prefix"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endfor %}
</wa-select>
</p>
<script src="{{ page.url }}index.js" type="module"></script>
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon>
Please note that not all combinations will look good — once youre mixing and matching, youre on your own!
</wa-callout>
{% endblock %}
{% block afterContent %}
{% markdown %}
## How to use your remixed theme { #remixed-usage }
You can import your remixed theme by importing the individual theme files from the Web Awesome CDN.
{# Note: If you change the order here, you need to update index.js too #}
{% set stylesheets = [
'styles/themes/default.css',
'styles/color/default.css',
'styles/themes/default/color.css',
'styles/themes/default/typography.css'
] %}
{% set stylesheet = 'styles/themes/default/color.css' %}
{% include 'import-stylesheet-code.md.njk' %}
{% endmarkdown %}
{% endblock %}

View File

@@ -1,18 +0,0 @@
#mix_and_match {
strong {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
margin-top: 1.2em;
}
wa-select::part(label) {
margin-block-end: 0;
}
wa-select[value='']::part(display-input),
wa-option[value=''] {
font-style: italic;
color: var(--wa-color-text-quiet);
}
}

View File

@@ -9,29 +9,7 @@ body,
overflow: hidden;
}
html.remixed {
.remix-link {
display: none;
}
}
html:not(.remixed) {
#mix_and_match {
display: none;
}
}
#mix_and_match {
font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet);
wa-icon {
vertical-align: middle;
}
}
.theme-showcase {
isolation: isolate;
background-color: var(--wa-color-surface-lowered);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-xl);

View File

@@ -3,5 +3,5 @@ title: Tailspin
description: Like a bird in flight, guiding you from there to here.
isPro: true
tags: pro
palette: vogue
defaultPalette: vogue
---

View File

@@ -1,5 +1,4 @@
{
"layout": "theme.njk",
"wide": true,
"tags": ["themes", "theme"]
}

View File

@@ -6,30 +6,6 @@ description: Ensure consistent use of color and readable contrast with Web Aweso
<style>
td { vertical-align: middle; }
.color-name {
font-weight: var(--wa-font-weight-semibold);
margin-block-end: var(--wa-space-2xs);
}
ul.color-group {
list-style: none;
margin: 0;
padding: 0;
}
.color-group {
align-items: start;
display: flex;
flex-wrap: nowrap;
gap: 0.25em;
}
.color-group + * {
margin-block-start: var(--wa-space-xl);
}
.color-preview {
flex: 1 1 auto;
}
.swatch {
border-color: transparent;
}
.color-mix-example {
background-image:
linear-gradient(to right,
@@ -49,31 +25,29 @@ Web Awesome's color system is made up of CSS custom properties to help with cons
Color is organized by three main categories:
- [Literal colors](/#literal-colors) that give familiar names to your starting [color palette](/docs/palettes/)
- [Literal colors](/#literal-colors) that give familiar names to your starting color palette
- [Foundational colors](/#foundational-colors) that lay the groundwork for your theme
- [Semantic colors](/#semantic-colors) that draw attention and convey meaning
## Literal Colors
Literal colors are defined by your theme's [color palette](/docs/palettes/) and are the lowest level color tokens in your theme. Each token is identified by a name, like red or gray, and a number based on the color's lightness. On this scale, 100 is equal to pure white and 0 is equal to pure black.
Literal colors are the lowest level color properties in your theme. Each color is identified by a name, like red or gray, and a number that roughly corresponds to the color's perceived lightness. On this scale, 100 is equal to pure white and 0 is equal to pure black.
You can use these numbers to ensure accessible color contrast per [WCAG 2.1 success criteria](https://www.w3.org/TR/WCAG21/#contrast-minimum):
Lightness values on this scale have a strong correlation to [relative luminance](https://www.w3.org/WAI/GL/wiki/Relative_luminance), which is used to calculate color contrast. To meet [WCAG 2.1 success criteria for minimum or enhanced contrast](https://www.w3.org/TR/WCAG21/#contrast-minimum), even across hues, calculate the difference between the lightness values of any two colors:
- A difference of 40 ensures a minimum 3:1 contrast ratio, suitable for large text and icons (AA)
- A difference of 50 ensures a minimum 4.5:1 contrast ratio, suitable for normal text (AA) and large text (AAA)
- A difference of 60 ensures a minimum 7:1 contrast ratio, suitable for all text (AAA)
Each Web Awesome palette defines seven literal colors each with 11 lightness values using the format `--wa-color-{hue}-{tint}`.
Web Awesome defines seven literal colors each with 11 lightness values using the format `--wa-color-{hue}-{tint}`.
{% for hue in ["red", "yellow", "green", "teal", "blue", "indigo", "violet", "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"] -%}
<li class="color-preview">
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div>
<div class="swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})"></div>
<small>{{ tint }}</small>
</li>
{%- endfor %}
@@ -189,3 +163,23 @@ Finally, each color is named according to how much attention it draws. Here, we
{%- endfor %}
{%- endfor %}
</table>
<script type="module">
const computedStyle = getComputedStyle(document.body)
document.querySelectorAll(".swatch").forEach((swatch) => {
let varName = swatch.getAttribute("value")
if (!varName) {
const bgColor = swatch.style.backgroundColor
varName = bgColor.replace(/^var\((--.*)\)$/, "$1")
}
const copyButton = Object.assign(document.createElement("wa-copy-button"), {
value: varName,
copyLabel: varName,
errorLabel: "Whoops, your browser doesn't support this!",
})
swatch.appendChild(copyButton)
})
</script>

View File

@@ -24,9 +24,22 @@ Some properties are boolean, so they only have true/false values. To activate a
## Events
You can listen for standard events such as `click`, `mouseover`, etc. as you normally would. In addition, some components have their own custom events. For example, you might listen to `wa-after-show` to determine when a dialog has been shown.
You can listen for standard events such as `click`, `mouseover`, etc. as you normally would. However, it's important to note that many events emitted within a component's shadow root will be [retargeted](https://dom.spec.whatwg.org/#retarget) to the host element. This may result in, for example, multiple `click` handlers executing even if the user clicks just once. Furthermore, `event.target` will point to the host element, making things even more confusing.
Custom Web Awesome events are prefixed with `wa-` to prevent collisions with standard events and other libraries. Refer to a component's documentation for a complete list of its events.
As a result, you should almost always listen for Web Awesome events instead. For example, instead of listening to `click` to determine when an `<wa-checkbox>` gets toggled, listen to `wa-change`.
```html
<wa-checkbox>Check me</wa-checkbox>
<script>
const checkbox = document.querySelector('wa-checkbox');
checkbox.addEventListener('wa-change', event => {
console.log(event.target.checked ? 'checked' : 'not checked');
});
</script>
```
All Web Awesome events are prefixed with `wa-` to prevent collisions with standard events and other libraries. Refer to a component's documentation for a complete list of its events.
## Methods

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.9",
"version": "3.0.0-alpha.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.9",
"version": "3.0.0-alpha.8",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
@@ -34,7 +34,6 @@
"chalk": "^5.3.0",
"change-case": "^4.1.2",
"chokidar": "^3.5.3",
"colorjs.io": "^0.6.0-alpha.1",
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.1",
"cspell": "^6.18.1",
@@ -5079,17 +5078,6 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
"node_modules/colorjs.io": {
"version": "0.6.0-alpha.1",
"resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.0-alpha.1.tgz",
"integrity": "sha512-c/h/8uAmPydQcriRdX8UTAFHj6SpSHFHBA8LvMikvYWAVApPTwg/pyOXNsGmaCBd6L/EeDlRHSNhTtnIFp/qsg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/color"
}
},
"node_modules/command-line-args": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/webawesome",
"description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.9",
"version": "3.0.0-alpha.8",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -54,7 +54,6 @@
"create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner --group default",
"test:component": "web-test-runner -- --watch --group",
"test:contrast": "cd src/styles/color && node contrast.test.js",
"test:watch": "web-test-runner --watch --group default",
"prettier": "prettier --check --log-level=warn .",
"prettier:fix": "prettier --write --log-level=warn .",
@@ -92,7 +91,6 @@
"chalk": "^5.3.0",
"change-case": "^4.1.2",
"chokidar": "^3.5.3",
"colorjs.io": "^0.6.0-alpha.1",
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.1",
"cspell": "^6.18.1",

View File

@@ -126,11 +126,7 @@ async function generateStyles() {
file.includes('themes/default') ||
file.includes('themes/awesome') ||
file.includes('themes/active') ||
file.includes('themes/glossy') ||
file.includes('themes/matter') ||
file.includes('themes/mellow') ||
file.includes('themes/playful') ||
file.includes('themes/premium') ||
file.includes('themes/tailspin') ||
file.includes('themes/brutalist')
) {

View File

@@ -24,16 +24,14 @@ for await (const component of components) {
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.path.replace(/\.js$/, '.js');
// We only want to wrap wa- prefixed events, because the others are native
const eventsToWrap = component.events?.filter(event => event.name.startsWith('wa-')) || [];
const eventImports = eventsToWrap
const eventImports = (component.events || [])
.map(event => `import type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventExports = eventsToWrap
const eventExports = (component.events || [])
.map(event => `export type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventNameImport = eventsToWrap.length > 0 ? `import { type EventName } from '@lit/react';` : ``;
const events = eventsToWrap
const eventNameImport = (component.events || []).length > 0 ? `import { type EventName } from '@lit/react';` : ``;
const events = (component.events || [])
.map(event => `${event.reactName}: '${event.name}' as EventName<${event.eventName}>`)
.join(',\n');

View File

@@ -318,13 +318,13 @@ describe('<wa-button>', () => {
});
describe('when using methods', () => {
it('should emit focus and blur when the button is focused and blurred', async () => {
it('should emit wa-focus and wa-blur when the button is focused and blurred', async () => {
const el = await fixture<WaButton>(html` <wa-button>Button</wa-button> `);
const focusHandler = sinon.spy();
const blurHandler = sinon.spy();
el.addEventListener('focus', focusHandler);
el.addEventListener('blur', blurHandler);
el.addEventListener('wa-focus', focusHandler);
el.addEventListener('wa-blur', blurHandler);
el.focus();
await waitUntil(() => focusHandler.calledOnce);

View File

@@ -2,10 +2,12 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { html, literal } from 'lit/static-html.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInvalidEvent } from '../../events/invalid.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/button.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -24,8 +26,8 @@ import styles from './button.css';
* @dependency wa-icon
* @dependency wa-spinner
*
* @event blur - Emitted when the button loses focus.
* @event focus - Emitted when the button gains focus.
* @event wa-blur - Emitted when the button loses focus.
* @event wa-focus - Emitted when the button gains focus.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @slot - The button's label.
@@ -70,8 +72,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
/** The button's visual appearance. */
@property({ reflect: true, default: 'accent' })
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'accent';
@property({ reflect: true }) appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'accent';
/** The button's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@@ -140,6 +141,14 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
/** Used to override the form owner's `target` attribute. */
@property({ attribute: 'formtarget' }) formTarget: '_self' | '_blank' | '_parent' | '_top' | string;
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
private handleClick() {
const form = this.getForm();
@@ -264,6 +273,8 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
role=${ifDefined(isLink ? undefined : 'button')}
aria-disabled=${this.disabled ? 'true' : 'false'}
tabindex=${this.disabled ? '-1' : '0'}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@invalid=${this.isButton() ? this.handleInvalid : null}
@click=${this.handleClick}
>

View File

@@ -1,35 +1,29 @@
:host {
--spacing: var(--wa-space-xl);
--border-width: var(--wa-panel-border-width);
--border-radius: var(--wa-panel-border-radius);
--inner-border-radius: calc(var(--border-radius) - var(--border-width));
display: flex;
flex-direction: column;
background-color: var(--wa-color-surface-default);
border-color: var(--wa-color-surface-border);
border-radius: var(--border-radius);
border-radius: var(--wa-panel-border-radius);
border-style: var(--wa-panel-border-style);
border-width: var(--wa-panel-border-width);
box-shadow: var(--wa-shadow-s);
border-width: var(--border-width);
color: var(--wa-color-text-normal);
}
.image,
:host(:not([with-image])) .header {
border-start-start-radius: var(--inner-border-radius);
border-start-end-radius: var(--inner-border-radius);
}
.image {
display: flex;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
margin: calc(-1 * var(--border-width));
overflow: hidden;
&::slotted(img) {
display: block;
width: 100%;
border-start-start-radius: inherit !important;
border-start-end-radius: inherit !important;
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
}
@@ -39,6 +33,11 @@
padding: calc(var(--spacing) / 2) var(--spacing);
}
:host(:not([with-image])) .header {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.body {
display: block;
padding: var(--spacing);
@@ -47,8 +46,8 @@
.footer {
display: block;
border-top: inherit;
border-end-start-radius: var(--inner-border-radius);
border-end-end-radius: var(--inner-border-radius);
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
padding: var(--spacing);
}

View File

@@ -19,8 +19,6 @@ import styles from './card.css';
* @csspart body - The container that wraps the card's main content.
* @csspart footer - The container that wraps the card's footer.
*
* @cssproperty --border-radius - The radius for the card's corners. Expects a single value. Defaults to `var(--wa-panel-border-radius)`.
* @cssproperty --border-width - The width of the card's borders. Expects a single value. Defaults to `var(--wa-panel-border-width)`.
* @cssproperty --spacing - The amount of space around and between sections of the card. Expects a single value.
*/
@customElement('wa-card')

View File

@@ -60,13 +60,13 @@ describe('<wa-checkbox>', () => {
expect(el.checkValidity()).to.be.true;
});
it('should emit change and input when clicked', async () => {
it('should emit wa-change and wa-input when clicked', async () => {
const el = await fixture<WaCheckbox>(html` <wa-checkbox></wa-checkbox> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.click();
await aTimeout(0);
await el.updateComplete;
@@ -76,13 +76,13 @@ describe('<wa-checkbox>', () => {
expect(el.checked).to.be.true;
});
it('should emit change and input when toggled with spacebar', async () => {
it('should emit wa-change and wa-input when toggled with spacebar', async () => {
const el = await fixture<WaCheckbox>(html` <wa-checkbox></wa-checkbox> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await el.updateComplete;
await sendKeys({ press: ' ' });
@@ -92,11 +92,11 @@ describe('<wa-checkbox>', () => {
expect(el.checked).to.be.true;
});
it('should not emit change or input when checked programmatically', async () => {
it('should not emit wa-change or wa-input when checked programmatically', async () => {
const el = await fixture<WaCheckbox>(html` <wa-checkbox></wa-checkbox> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.checked = true;
await el.updateComplete;
await aTimeout(0);

View File

@@ -4,10 +4,14 @@ import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/checkbox.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -25,10 +29,10 @@ import styles from './checkbox.css';
* @slot - The checkbox's label.
* @slot hint - Text that describes how to use the checkbox. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the checkbox loses focus.
* @event change - Emitted when the checked state changes.
* @event focus - Emitted when the checkbox gains focus.
* @event input - Emitted when the checkbox receives input.
* @event wa-blur - Emitted when the checkbox loses focus.
* @event wa-change - Emitted when the checked state changes.
* @event wa-focus - Emitted when the checkbox gains focus.
* @event wa-input - Emitted when the checkbox receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart base - The component's label .
@@ -133,7 +137,19 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
this.hasInteracted = true;
this.checked = !this.checked;
this.indeterminate = false;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleInput() {
this.dispatchEvent(new WaInputEvent());
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
@watch('defaultChecked')
@@ -217,37 +233,47 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
// https://bugs.chromium.org/p/chromium/issues/detail?id=1413733
//
return html`
<label part="base">
<span part="control">
<input
class="input"
type="checkbox"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${this.name}
value=${ifDefined(this._value)}
.indeterminate=${live(this.indeterminate)}
.checked=${live(this.checked)}
.disabled=${this.disabled}
.required=${this.required}
aria-checked=${this.checked ? 'true' : 'false'}
aria-describedby="hint"
@click=${this.handleClick}
/>
<wa-icon part="${iconState}-icon icon" library="system" name=${iconName}></wa-icon>
</span>
<slot part="label"></slot>
</label>
<slot
name="hint"
aria-hidden=${hasHint ? 'false' : 'true'}
class="${classMap({ 'has-slotted': hasHint })}"
id="hint"
part="hint"
>${this.hint}</slot
<div
class=${classMap({
'form-control--has-hint': hasHint,
'form-control': true,
})}
>
<label part="base">
<span class="control">
<input
class="input"
type="checkbox"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${this.name}
value=${ifDefined(this._value)}
.indeterminate=${live(this.indeterminate)}
.checked=${live(this.checked)}
.disabled=${this.disabled}
.required=${this.required}
aria-checked=${this.checked ? 'true' : 'false'}
aria-describedby="hint"
@click=${this.handleClick}
@input=${this.handleInput}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
/>
<wa-icon part="${iconState}-icon icon" library="system" name=${iconName}></wa-icon>
</span>
<slot part="label"></slot>
</label>
<slot
name="hint"
aria-hidden=${hasHint ? 'false' : 'true'}
class="${classMap({ 'has-slotted': hasHint })}"
id="hint"
part="hint"
>${this.hint}</slot
>
</div>
`;
}
}

View File

@@ -14,31 +14,31 @@ describe('<wa-color-picker>', () => {
for (const fixture of fixtures) {
describe(`with "${fixture.type}" rendering`, () => {
describe('when the value changes', () => {
it('should not emit change or input when the value is changed programmatically', async () => {
it('should not emit wa-change or wa-input when the value is changed programmatically', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const color = 'rgb(255, 204, 0)';
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('change should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-change should not be emitted'));
el.value = color;
await el.updateComplete;
});
it('should emit change and input when the color grid selector is moved', async () => {
it('should emit wa-change and wa-input when the color grid selector is moved', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const grid = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
await el.updateComplete;
// Simulate a drag event. "change" should not fire until we stop dragging.
// Simulate a drag event. "wa-change" should not fire until we stop dragging.
await dragElement(grid, 2, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
@@ -53,20 +53,20 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledTwice;
});
it('should emit change and input when the hue slider is moved', async () => {
it('should emit wa-change and wa-input when the hue slider is moved', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
// Simulate a drag event. "change" should not fire until we stop dragging.
// Simulate a drag event. "wa-change" should not fire until we stop dragging.
await dragElement(slider, 20, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
@@ -85,20 +85,20 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when the opacity slider is moved', async () => {
it('should emit wa-change and wa-input when the opacity slider is moved', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
// Simulate a drag event. "change" should not fire until we stop dragging.
// Simulate a drag event. "wa-change" should not fire until we stop dragging.
await dragElement(slider, 2, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
@@ -115,15 +115,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledTwice;
});
it('should emit change and input when toggling the format', async () => {
it('should emit wa-change and wa-input when toggling the format', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker value="#fff"></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const formatButton = el.shadowRoot!.querySelector<HTMLElement>('[part~="format-button"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -160,7 +160,7 @@ describe('<wa-color-picker>', () => {
expect(getComputedStyle(swatches[2]).backgroundColor).to.equal('rgb(0, 0, 255)');
});
it('should emit change and input when clicking on a swatch', async () => {
it('should emit wa-change and wa-input when clicking on a swatch', async () => {
const el = await fixture<WaColorPicker>(html`
<wa-color-picker swatches="red; green; blue;"></wa-color-picker>
`);
@@ -169,8 +169,8 @@ describe('<wa-color-picker>', () => {
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -181,15 +181,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when selecting a color with the keyboard', async () => {
it('should emit wa-change and wa-input when selecting a color with the keyboard', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const gridHandle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -201,15 +201,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when selecting a color with the keyboard', async () => {
it('should emit wa-change and wa-input when selecting a color with the keyboard', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -221,15 +221,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when selecting hue with the keyboard', async () => {
it('should emit wa-change and wa-input when selecting hue with the keyboard', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"] > span')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -241,15 +241,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when selecting opacity with the keyboard', async () => {
it('should emit wa-change and wa-input when selecting opacity with the keyboard', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"] > span')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -261,15 +261,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when entering a value in the color input and pressing enter', async () => {
it('should emit wa-change and wa-input when entering a value in the color input and pressing enter', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -283,15 +283,15 @@ describe('<wa-color-picker>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should emit change and input when entering a value in the color input and blurring the field', async () => {
it('should emit wa-change and wa-input when entering a value in the color input and blurring the field', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
@@ -311,8 +311,8 @@ describe('<wa-color-picker>', () => {
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.swatches = ['#fff'];
await el.updateComplete;
@@ -354,14 +354,14 @@ describe('<wa-color-picker>', () => {
it.skip('should display a color with opacity when an initial value with opacity is provided', async () => {
const el = await fixture<WaColorPicker>(html` <wa-color-picker opacity value="#ff000050"></wa-color-picker> `);
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
const previewButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="preview"]')!;
const previewButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="preview"]');
const previewColor = getComputedStyle(previewButton).getPropertyValue('--preview-color');
expect(trigger.style.color).to.equal('rgba(255, 0, 0, 0.314)');
expect(previewColor).to.equal('#ff000050');
});
it.skip('should emit focus when rendered as a dropdown and focused', async () => {
it.skip('should emit wa-focus when rendered as a dropdown and focused', async () => {
const el = await fixture<WaColorPicker>(html`
<div>
<wa-color-picker></wa-color-picker>
@@ -374,8 +374,8 @@ describe('<wa-color-picker>', () => {
const focusHandler = sinon.spy();
const blurHandler = sinon.spy();
colorPicker.addEventListener('focus', focusHandler);
colorPicker.addEventListener('blur', blurHandler);
colorPicker.addEventListener('wa-focus', focusHandler);
colorPicker.addEventListener('wa-blur', blurHandler);
await clickOnElement(trigger);
await colorPicker.updateComplete;
@@ -391,8 +391,8 @@ describe('<wa-color-picker>', () => {
const focusHandler = sinon.spy();
const blurHandler = sinon.spy();
colorPicker.addEventListener('focus', focusHandler);
colorPicker.addEventListener('blur', blurHandler);
colorPicker.addEventListener('wa-focus', focusHandler);
colorPicker.addEventListener('wa-blur', blurHandler);
// Focus
colorPicker.focus();

View File

@@ -5,13 +5,17 @@ import { customElement, eventOptions, property, query, state } from 'lit/decorat
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { WaInvalidEvent } from '../../events/invalid.js';
import { drag } from '../../internal/drag.js';
import { clamp } from '../../internal/math.js';
import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import formControlStyles from '../../styles/shadow/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
import visuallyHidden from '../../styles/utilities/visually-hidden.css';
@@ -50,10 +54,10 @@ declare const EyeDropper: EyeDropperConstructor;
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
* @slot hint - The color picker's form hint. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the color picker loses focus.
* @event change - Emitted when the color picker's value changes.
* @event focus - Emitted when the color picker receives focus.
* @event input - Emitted when the color picker receives input.
* @event wa-blur - Emitted when the color picker loses focus.
* @event wa-change - Emitted when the color picker's value changes.
* @event wa-focus - Emitted when the color picker receives focus.
* @event wa-input - Emitted when the color picker receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart base - The component's base wrapper.
@@ -262,10 +266,12 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
private handleFocusIn = () => {
this.hasFocus = true;
this.dispatchEvent(new WaFocusEvent());
};
private handleFocusOut = () => {
this.hasFocus = false;
this.dispatchEvent(new WaBlurEvent());
};
private handleFormatToggle() {
@@ -273,8 +279,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv';
this.setColor(this.value || '');
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
private handleAlphaDrag(event: PointerEvent) {
@@ -294,13 +300,13 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
if (this.value !== currentValue) {
currentValue = this.value;
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaInputEvent());
}
},
onStop: () => {
if (this.value !== initialValue) {
initialValue = this.value;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
},
initialEvent: event,
@@ -324,13 +330,13 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
if (this.value !== currentValue) {
currentValue = this.value;
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaInputEvent());
}
},
onStop: () => {
if (this.value !== initialValue) {
initialValue = this.value;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
},
initialEvent: event,
@@ -357,14 +363,14 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
if (this.value !== currentValue) {
currentValue = this.value;
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaInputEvent());
}
},
onStop: () => {
this.isDraggingGridHandle = false;
if (this.value !== initialValue) {
initialValue = this.value;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
},
initialEvent: event,
@@ -400,8 +406,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
@@ -434,8 +440,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
@@ -468,12 +474,12 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
private handleInputChange(event: Event) {
private handleInputChange(event: WaChangeEvent) {
const target = event.target as HTMLInputElement;
const oldValue = this.value;
@@ -488,15 +494,15 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
private handleInputInput(event: InputEvent) {
private handleInputInput(event: WaInputEvent) {
this.updateValidity();
// Prevent the `<wa-input>` element's `input` event from bubbling up
// Prevent the `<wa-input>` element's `wa-input` event from bubbling up
event.stopPropagation();
}
@@ -509,8 +515,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
this.input.value = this.value;
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
setTimeout(() => this.input.select());
@@ -685,8 +691,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
this.setColor(colorSelectionResult.sRGBHex);
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
})
.catch(() => {
@@ -701,8 +707,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
this.setColor(color);
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
}
@@ -1002,10 +1008,10 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
?disabled=${this.disabled}
aria-label=${this.localize.term('currentValue')}
@keydown=${this.handleInputKeyDown}
@change=${this.handleInputChange}
@input=${this.handleInputInput}
@blur=${this.stopNestedEventPropagation}
@focus=${this.stopNestedEventPropagation}
@wa-change=${this.handleInputChange}
@wa-input=${this.handleInputInput}
@wa-blur=${this.stopNestedEventPropagation}
@wa-focus=${this.stopNestedEventPropagation}
></wa-input>
<wa-button-group>
@@ -1023,8 +1029,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
caret:format-button__caret
"
@click=${this.handleFormatToggle}
@blur=${this.stopNestedEventPropagation}
@focus=${this.stopNestedEventPropagation}
@wa-blur=${this.stopNestedEventPropagation}
@wa-focus=${this.stopNestedEventPropagation}
>
${this.setLetterCase(this.format)}
</wa-button>
@@ -1043,8 +1049,8 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
caret:eye-dropper-button__caret
"
@click=${this.handleEyeDropper}
@blur=${this.stopNestedEventPropagation}
@focus=${this.stopNestedEventPropagation}
@wa-blur=${this.stopNestedEventPropagation}
@wa-focus=${this.stopNestedEventPropagation}
>
<wa-icon
library="system"

View File

@@ -146,13 +146,13 @@ describe('<wa-icon-button>', () => {
});
describe('when using methods', () => {
it('should emit focus and blur when the button is focused and blurred', async () => {
it('should emit wa-focus and wa-blur when the button is focused and blurred', async () => {
const el = await fixture<WaIconButton>(html` <wa-icon-button></wa-icon-button> `);
const focusHandler = sinon.spy();
const blurHandler = sinon.spy();
el.addEventListener('focus', focusHandler);
el.addEventListener('blur', blurHandler);
el.addEventListener('wa-focus', focusHandler);
el.addEventListener('wa-blur', blurHandler);
el.focus();
await waitUntil(() => focusHandler.calledOnce);

View File

@@ -2,7 +2,9 @@ import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { html, literal } from 'lit/static-html.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import '../icon/icon.js';
import styles from './icon-button.css';
@@ -14,8 +16,8 @@ import styles from './icon-button.css';
*
* @dependency wa-icon
*
* @event blur - Emitted when the icon button loses focus.
* @event focus - Emitted when the icon button gains focus.
* @event wa-blur - Emitted when the icon button loses focus.
* @event wa-focus - Emitted when the icon button gains focus.
*
* @cssproperty --background-color-hover - The color of the button's background on hover.
*
@@ -69,6 +71,14 @@ export default class WaIconButton extends WebAwesomeFormAssociatedElement {
/** Disables the button. */
@property({ type: Boolean }) disabled = false;
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
private handleClick(event: MouseEvent) {
if (this.disabled) {
event.preventDefault();
@@ -112,6 +122,8 @@ export default class WaIconButton extends WebAwesomeFormAssociatedElement {
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-label="${this.label}"
tabindex=${this.disabled ? '-1' : '0'}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}
>
<wa-icon

View File

@@ -17,13 +17,16 @@
.before,
.after {
display: block;
pointer-events: none;
}
&::slotted(img),
&::slotted(svg) {
display: block;
max-width: 100% !important;
height: auto;
}
.before::slotted(img),
.after::slotted(img),
.before::slotted(svg),
.after::slotted(svg) {
display: block;
max-width: 100% !important;
height: auto;
}
.after {

View File

@@ -37,7 +37,7 @@ describe('<wa-image-comparer>', () => {
`);
const handler = sinon.spy();
el.addEventListener('change', handler, { once: true });
el.addEventListener('wa-change', handler, { once: true });
el.position = 40;
await el.updateComplete;

View File

@@ -1,6 +1,7 @@
import { html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { WaChangeEvent } from '../../events/change.js';
import { drag } from '../../internal/drag.js';
import { clamp } from '../../internal/math.js';
import { watch } from '../../internal/watch.js';
@@ -21,7 +22,7 @@ import styles from './image-comparer.css';
* @slot after - The after image, an `<img>` or `<svg>` element.
* @slot handle - The icon used inside the handle.
*
* @event change - Emitted when the position changes.
* @event wa-change - Emitted when the position changes.
*
* @csspart base - The component's base wrapper.
* @csspart before - The container that wraps the before image.
@@ -91,7 +92,7 @@ export default class WaImageComparer extends WebAwesomeElement {
@watch('position', { waitUntilFirstUpdate: true })
handlePositionChange() {
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
render() {

View File

@@ -60,7 +60,7 @@ describe('<wa-input>', () => {
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
const focusHandler = sinon.spy();
el.addEventListener('focus', focusHandler);
el.addEventListener('wa-focus', focusHandler);
(label as HTMLLabelElement).click();
await waitUntil(() => focusHandler.calledOnce);
@@ -312,13 +312,13 @@ describe('<wa-input>', () => {
});
describe('when the value changes', () => {
it('should emit change and input when the user types in the input', async () => {
it('should emit wa-change and wa-input when the user types in the input', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
const inputHandler = sinon.spy();
const changeHandler = sinon.spy();
el.addEventListener('input', inputHandler);
el.addEventListener('change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.focus();
await sendKeys({ type: 'abc' });
el.blur();
@@ -328,21 +328,21 @@ describe('<wa-input>', () => {
expect(inputHandler).to.have.been.calledThrice;
});
it('should not emit change or input when the value is set programmatically', async () => {
it('should not emit wa-change or wa-input when the value is set programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input></wa-input> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.value = 'abc';
await el.updateComplete;
});
it('should not emit change or input when calling setRangeText()', async () => {
it('should not emit wa-change or wa-input when calling setRangeText()', async () => {
const el = await fixture<WaInput>(html` <wa-input value="hi there"></wa-input> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.focus();
el.setSelectionRange(0, 2);
el.setRangeText('hello');
@@ -399,21 +399,21 @@ describe('<wa-input>', () => {
expect(el.value).to.equal('0');
});
it('should not emit input or change when stepUp() is called programmatically', async () => {
it('should not emit wa-input or wa-change when stepUp() is called programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepUp();
await el.updateComplete;
});
it('should not emit input and change when stepDown() is called programmatically', async () => {
it('should not emit wa-input and wa-change when stepDown() is called programmatically', async () => {
const el = await fixture<WaInput>(html` <wa-input type="number" step="2" value="2"></wa-input> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepDown();
await el.updateComplete;

View File

@@ -1,13 +1,17 @@
import { html, isServer, type PropertyValues } from 'lit';
import { html, isServer } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaClearEvent } from '../../events/clear.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { HasSlotController } from '../../internal/slot.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/input.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
@@ -33,11 +37,11 @@ import styles from './input.css';
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
* @slot hint - Text that describes how to use the input. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event change - Emitted when an alteration to the control's value is committed by the user.
* @event focus - Emitted when the control gains focus.
* @event input - Emitted when the control receives input.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-change - Emitted when an alteration to the control's value is committed by the user.
* @event wa-clear - Emitted when the clear button is activated.
* @event wa-focus - Emitted when the control gains focus.
* @event wa-input - Emitted when the control receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart label - The label
@@ -53,8 +57,6 @@ import styles from './input.css';
* @cssproperty --border-color - The color of the input's borders.
* @cssproperty --border-width - The width of the input's borders. Expects a single value.
* @cssproperty --box-shadow - The shadow effects around the edges of the input.
*
* @cssstate blank - The input is empty.
*/
@customElement('wa-input')
export default class WaInput extends WebAwesomeFormAssociatedElement {
@@ -66,7 +68,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
return [...super.validators, MirrorValidator()];
}
assumeInteractionOn = ['blur', 'input'];
assumeInteractionOn = ['wa-blur', 'wa-input'];
private readonly hasSlotController = new HasSlotController(this, 'hint', 'label');
private readonly localize = new LocalizeController(this);
@@ -223,9 +225,13 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
*/
@property({ attribute: 'with-hint', type: Boolean }) withHint = false;
private handleChange(event: Event) {
this.dispatchComposedEvent(event);
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleChange() {
this.value = this.input.value;
this.dispatchEvent(new WaChangeEvent());
}
private handleClearClick(event: MouseEvent) {
@@ -234,15 +240,20 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
if (this.value !== '') {
this.value = '';
this.dispatchEvent(new WaClearEvent());
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaInputEvent());
this.dispatchEvent(new WaChangeEvent());
}
this.input.focus();
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
private handleInput() {
this.value = this.input.value;
this.dispatchEvent(new WaInputEvent());
}
private handleKeyDown(event: KeyboardEvent) {
@@ -297,14 +308,6 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
this.passwordVisible = !this.passwordVisible;
}
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
}
}
@watch('step', { waitUntilFirstUpdate: true })
handleStepChange() {
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
@@ -432,6 +435,8 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
@change=${this.handleChange}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
${isClearIconVisible
@@ -460,15 +465,15 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
@click=${this.handlePasswordToggle}
tabindex="-1"
>
${!this.passwordVisible
${this.passwordVisible
? html`
<slot name="show-password-icon">
<wa-icon name="eye" library="system" variant="regular"></wa-icon>
<wa-icon name="eye-slash" library="system" variant="regular"></wa-icon>
</slot>
`
: html`
<slot name="hide-password-icon">
<wa-icon name="eye-slash" library="system" variant="regular"></wa-icon>
<wa-icon name="eye" library="system" variant="regular"></wa-icon>
</slot>
`}
</button>

View File

@@ -2,9 +2,11 @@ import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { html } from 'lit/static-html.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { HasSlotController } from '../../internal/slot.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/button.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -22,8 +24,8 @@ import styles from './radio-button.css';
* @slot prefix - A presentational prefix icon or similar element.
* @slot suffix - A presentational suffix icon or similar element.
*
* @event blur - Emitted when the button loses focus.
* @event focus - Emitted when the button gains focus.
* @event wa-blur - Emitted when the button loses focus.
* @event wa-focus - Emitted when the button gains focus.
*
* @cssproperty --background-color - The button's background color.
* @cssproperty --background-color-active - The button's background color when active.
@@ -105,6 +107,10 @@ export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
this.setAttribute('role', 'presentation');
}
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleClick(e: MouseEvent) {
if (this.disabled) {
e.preventDefault();
@@ -115,6 +121,10 @@ export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
this.checked = true;
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
@@ -154,6 +164,8 @@ export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
aria-disabled=${this.disabled}
type="button"
value=${ifDefined(this.value)}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}
>
<slot name="prefix" part="prefix" class="prefix"></slot>

View File

@@ -2,6 +2,7 @@ import { aTimeout, expect, oneEvent } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import { html } from 'lit';
import sinon from 'sinon';
import type { WaChangeEvent } from '../../events/change.js';
import { clickOnElement } from '../../internal/test.js';
import { fixtures } from '../../internal/test/fixture.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
@@ -325,7 +326,7 @@ describe('<wa-radio-group>', () => {
const validFocusHandler = sinon.spy();
Array.from(el.querySelectorAll<WaRadio>('wa-radio')).forEach(radio =>
radio.addEventListener('focus', validFocusHandler),
radio.addEventListener('wa-focus', validFocusHandler),
);
expect(validFocusHandler).to.not.have.been.called;
@@ -349,8 +350,8 @@ describe('<wa-radio-group>', () => {
const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-1')!;
disabledRadio.addEventListener('focus', invalidFocusHandler);
validRadio.addEventListener('focus', validFocusHandler);
disabledRadio.addEventListener('wa-focus', invalidFocusHandler);
validRadio.addEventListener('wa-focus', validFocusHandler);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.not.have.been.called;
@@ -377,8 +378,8 @@ describe('<wa-radio-group>', () => {
const disabledRadio = el.querySelector('#radio-0')!;
const validRadio = el.querySelector('#radio-2')!;
disabledRadio.addEventListener('focus', invalidFocusHandler);
validRadio.addEventListener('focus', validFocusHandler);
disabledRadio.addEventListener('wa-focus', invalidFocusHandler);
validRadio.addEventListener('wa-focus', validFocusHandler);
expect(invalidFocusHandler).to.not.have.been.called;
expect(validFocusHandler).to.not.have.been.called;
@@ -393,7 +394,7 @@ describe('<wa-radio-group>', () => {
});
describe('when the value changes', () => {
it('should emit change when toggled with the arrow keys', async () => {
it('should emit wa-change when toggled with the arrow keys', async () => {
const radioGroup = await fixture<WaRadioGroup>(html`
<wa-radio-group>
<wa-radio id="radio-1" value="1"></wa-radio>
@@ -404,8 +405,8 @@ describe('<wa-radio-group>', () => {
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
radioGroup.addEventListener('change', changeHandler);
radioGroup.addEventListener('input', inputHandler);
radioGroup.addEventListener('wa-change', changeHandler);
radioGroup.addEventListener('wa-input', inputHandler);
firstRadio.focus();
await sendKeys({ press: 'ArrowRight' });
await radioGroup.updateComplete;
@@ -415,7 +416,7 @@ describe('<wa-radio-group>', () => {
expect(radioGroup.value).to.equal('2');
});
it('should emit change and input when clicked', async () => {
it('should emit wa-change and wa-input when clicked', async () => {
const radioGroup = await fixture<WaRadioGroup>(html`
<wa-radio-group>
<wa-radio id="radio-1" value="1"></wa-radio>
@@ -424,12 +425,12 @@ describe('<wa-radio-group>', () => {
`);
const radio = radioGroup.querySelector<WaRadio>('#radio-1')!;
setTimeout(() => radio.click());
const event = await oneEvent(radioGroup, 'change');
const event = (await oneEvent(radioGroup, 'wa-change')) as WaChangeEvent;
expect(event.target).to.equal(radioGroup);
expect(radioGroup.value).to.equal('1');
});
it('should emit change and input when toggled with spacebar', async () => {
it('should emit wa-change and wa-input when toggled with spacebar', async () => {
const radioGroup = await fixture<WaRadioGroup>(html`
<wa-radio-group>
<wa-radio id="radio-1" value="1"></wa-radio>
@@ -439,12 +440,12 @@ describe('<wa-radio-group>', () => {
const radio = radioGroup.querySelector<WaRadio>('#radio-1')!;
radio.focus();
setTimeout(() => sendKeys({ press: ' ' }));
const event = await oneEvent(radioGroup, 'change');
const event = (await oneEvent(radioGroup, 'wa-change')) as WaChangeEvent;
expect(event.target).to.equal(radioGroup);
expect(radioGroup.value).to.equal('1');
});
it('should not emit change or input when the value is changed programmatically', async () => {
it('should not emit wa-change or wa-input when the value is changed programmatically', async () => {
const radioGroup = await fixture<WaRadioGroup>(html`
<wa-radio-group value="1">
<wa-radio id="radio-1" value="1"></wa-radio>
@@ -452,8 +453,8 @@ describe('<wa-radio-group>', () => {
</wa-radio-group>
`);
radioGroup.addEventListener('change', () => expect.fail('change should not be emitted'));
radioGroup.addEventListener('input', () => expect.fail('input should not be emitted'));
radioGroup.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
radioGroup.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
radioGroup.value = '2';
await radioGroup.updateComplete;
});

View File

@@ -1,11 +1,13 @@
import { html, isServer } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaInputEvent } from '../../events/input.js';
import { uniqueId } from '../../internal/math.js';
import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import formControlStyles from '../../styles/shadow/form-control.css';
import buttonGroupStyles from '../../styles/utilities/button-group.css';
import sizeStyles from '../../styles/utilities/size.css';
@@ -27,8 +29,8 @@ import styles from './radio-group.css';
* attribute.
* @slot hint - Text that describes how to use the radio group. Alternatively, you can use the `hint` attribute.
*
* @event change - Emitted when the radio group's selected value changes.
* @event input - Emitted when the radio group receives user input.
* @event wa-change - Emitted when the radio group's selected value changes.
* @event wa-input - Emitted when the radio group receives user input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart form-control - The form control that wraps the label, input, and hint.
@@ -157,8 +159,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
};
@@ -288,8 +290,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
event.preventDefault();

View File

@@ -1,8 +1,10 @@
import { html, isServer } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/radio.css';
import sizeStyles from '../../styles/utilities/size.css';
import '../icon/icon.js';
@@ -18,8 +20,8 @@ import styles from './radio.css';
*
* @slot - The radio's label.
*
* @event blur - Emitted when the control loses focus.
* @event focus - Emitted when the control gains focus.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-focus - Emitted when the control gains focus.
*
* @csspart base - The component's base wrapper.
* @csspart control - The circular container that wraps the radio's checked state.
@@ -67,6 +69,8 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
super();
if (!isServer) {
this.addEventListener('click', this.handleClick);
this.addEventListener('blur', this.handleBlur);
this.addEventListener('focus', this.handleFocus);
}
}
@@ -75,6 +79,14 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
this.setInitialAttributes();
}
private handleBlur = () => {
this.dispatchEvent(new WaBlurEvent());
};
private handleFocus = () => {
this.dispatchEvent(new WaFocusEvent());
};
private setInitialAttributes() {
this.setAttribute('role', 'radio');
this.tabIndex = 0;

View File

@@ -54,12 +54,12 @@ describe('<wa-rating>', () => {
expect(base.getAttribute('aria-valuenow')).to.equal('3');
});
it('should emit change when clicked', async () => {
it('should emit wa-change when clicked', async () => {
const el = await fixture<WaRating>(html` <wa-rating></wa-rating> `);
const lastSymbol = el.shadowRoot!.querySelector<HTMLSpanElement>('.symbol:last-child')!;
const changeHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('wa-change', changeHandler);
await clickOnElement(lastSymbol);
await el.updateComplete;
@@ -68,11 +68,11 @@ describe('<wa-rating>', () => {
expect(el.value).to.equal(5);
});
it('should emit change when the value is changed with the keyboard', async () => {
it('should emit wa-change when the value is changed with the keyboard', async () => {
const el = await fixture<WaRating>(html` <wa-rating></wa-rating> `);
const changeHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('wa-change', changeHandler);
el.focus();
await el.updateComplete;
await sendKeys({ press: 'ArrowRight' });
@@ -82,12 +82,12 @@ describe('<wa-rating>', () => {
expect(el.value).to.equal(1);
});
it('should not emit change when disabled', async () => {
it('should not emit wa-change when disabled', async () => {
const el = await fixture<WaRating>(html` <wa-rating value="5" disabled></wa-rating> `);
const lastSymbol = el.shadowRoot!.querySelector<HTMLSpanElement>('.symbol:last-child')!;
const changeHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('wa-change', changeHandler);
await clickOnElement(lastSymbol);
await el.updateComplete;
@@ -96,9 +96,9 @@ describe('<wa-rating>', () => {
expect(el.value).to.equal(5);
});
it('should not emit change when the value is changed programmatically', async () => {
it('should not emit wa-change when the value is changed programmatically', async () => {
const el = await fixture<WaRating>(html` <wa-rating label="Test" value="1"></wa-rating> `);
el.addEventListener('change', () => expect.fail('change incorrectly emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change incorrectly emitted'));
el.value = 5;
await el.updateComplete;
});

View File

@@ -3,6 +3,7 @@ import { customElement, eventOptions, property, query, state } from 'lit/decorat
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaHoverEvent } from '../../events/hover.js';
import { clamp } from '../../internal/math.js';
import { watch } from '../../internal/watch.js';
@@ -19,7 +20,7 @@ import styles from './rating.css';
*
* @dependency wa-icon
*
* @event change - Emitted when the rating's value changes.
* @event wa-change - Emitted when the rating's value changes.
* @event {{ phase: 'start' | 'move' | 'end', value: number }} wa-hover - Emitted when the user hovers over a value. The
* `phase` property indicates when hovering starts, moves to a new value, or ends. The `value` property tells what the
* rating's value would be if the user were to commit to the hovered value.
@@ -95,7 +96,7 @@ export default class WaRating extends WebAwesomeElement {
}
this.setValue(this.getValueFromMousePosition(event));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
private setValue(newValue: number) {
@@ -139,7 +140,7 @@ export default class WaRating extends WebAwesomeElement {
}
if (this.value !== oldValue) {
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
}
@@ -172,7 +173,7 @@ export default class WaRating extends WebAwesomeElement {
private handleTouchEnd(event: TouchEvent) {
this.isHovering = false;
this.setValue(this.hoverValue);
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
// Prevent click on mobile devices
event.preventDefault();

View File

@@ -107,7 +107,7 @@ describe('<wa-select>', () => {
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
const submitHandler = sinon.spy();
el.addEventListener('focus', submitHandler);
el.addEventListener('wa-focus', submitHandler);
(label as HTMLLabelElement).click();
await waitUntil(() => submitHandler.calledOnce);
@@ -115,7 +115,7 @@ describe('<wa-select>', () => {
});
describe('when the value changes', () => {
it('should emit change when the value is changed with the mouse', async () => {
it('should emit wa-change when the value is changed with the mouse', async () => {
const el = await fixture<WaSelect>(html`
<wa-select value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -132,8 +132,8 @@ describe('<wa-select>', () => {
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
await el.show();
await clickOnElement(secondOption);
@@ -144,7 +144,7 @@ describe('<wa-select>', () => {
expect(el.value).to.equal('option-2');
});
it('should emit change and input when the value is changed with the keyboard', async () => {
it('should emit wa-change and wa-input when the value is changed with the keyboard', async () => {
const el = await fixture<WaSelect>(html`
<wa-select value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -155,8 +155,8 @@ describe('<wa-select>', () => {
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await el.updateComplete;
@@ -175,7 +175,7 @@ describe('<wa-select>', () => {
expect(el.value).to.equal('option-3');
});
it('should not emit change or input when the value is changed programmatically', async () => {
it('should not emit wa-change or wa-input when the value is changed programmatically', async () => {
const el = await fixture<WaSelect>(html`
<wa-select value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -184,14 +184,14 @@ describe('<wa-select>', () => {
</wa-select>
`);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.value = 'option-2';
await el.updateComplete;
});
it('should emit change and input with the correct validation message when the value changes', async () => {
it('should emit wa-change and wa-input with the correct validation message when the value changes', async () => {
const el = await fixture<WaSelect>(html`
<wa-select required>
<wa-option value="option-1">Option 1</wa-option>
@@ -206,8 +206,8 @@ describe('<wa-select>', () => {
}
});
el.addEventListener('change', handler);
el.addEventListener('input', handler);
el.addEventListener('wa-change', handler);
el.addEventListener('wa-input', handler);
await clickOnElement(el);
await aTimeout(500);
@@ -532,7 +532,7 @@ describe('<wa-select>', () => {
expect(displayInput.value).to.equal('updated');
});
it('should emit focus and blur when receiving and losing focus', async () => {
it('should emit wa-focus and wa-blur when receiving and losing focus', async () => {
const el = await fixture<WaSelect>(html`
<wa-select value="option-1">
<wa-option value="option-1">Option 1</wa-option>
@@ -543,8 +543,8 @@ describe('<wa-select>', () => {
const focusHandler = sinon.spy();
const blurHandler = sinon.spy();
el.addEventListener('focus', focusHandler);
el.addEventListener('blur', blurHandler);
el.addEventListener('wa-focus', focusHandler);
el.addEventListener('wa-blur', blurHandler);
el.focus();
await el.updateComplete;
@@ -574,7 +574,7 @@ describe('<wa-select>', () => {
expect(clearHandler).to.have.been.calledOnce;
});
it('should emit change and input when a tag is removed', async () => {
it('should emit wa-change and wa-input when a tag is removed', async () => {
const el = await fixture<WaSelect>(html`
<wa-select value="option-1 option-2 option-3" multiple>
<wa-option value="option-1">Option 1</wa-option>
@@ -587,8 +587,8 @@ describe('<wa-select>', () => {
const tag = el.shadowRoot!.querySelector('[part~="tag"]')!;
const removeButton = tag.shadowRoot!.querySelector('[part~="remove-button"]')!;
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
// The offsets are a funky hack for Firefox.
await clickOnElement(removeButton, 'center', 1, 1);

View File

@@ -1,12 +1,16 @@
import type { PropertyValues, TemplateResult } from 'lit';
import type { TemplateResult } from 'lit';
import { html, isServer } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaClearEvent } from '../../events/clear.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaHideEvent } from '../../events/hide.js';
import { WaInputEvent } from '../../events/input.js';
import type { WaRemoveEvent } from '../../events/remove.js';
import { WaShowEvent } from '../../events/show.js';
import { animateWithClass } from '../../internal/animate.js';
@@ -15,7 +19,7 @@ import { scrollIntoView } from '../../internal/scroll.js';
import { HasSlotController } from '../../internal/slot.js';
import { RequiredValidator } from '../../internal/validators/required-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/select.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
@@ -46,11 +50,11 @@ import styles from './select.css';
* @slot expand-icon - The icon to show when the control is expanded and collapsed. Rotates on open and close.
* @slot hint - Text that describes how to use the input. Alternatively, you can use the `hint` attribute.
*
* @event change - Emitted when the control's value changes.
* @event input - Emitted when the control receives input.
* @event focus - Emitted when the control gains focus.
* @event blur - Emitted when the control loses focus.
* @event wa-change - Emitted when the control's value changes.
* @event wa-clear - Emitted when the control's value is cleared.
* @event wa-input - Emitted when the control receives input.
* @event wa-focus - Emitted when the control gains focus.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-show - Emitted when the select's menu opens.
* @event wa-after-show - Emitted after the select's menu opens and all animations are complete.
* @event wa-hide - Emitted when the select's menu closes.
@@ -79,8 +83,6 @@ import styles from './select.css';
* @cssproperty --border-color - The border color of the select's combobox.
* @cssproperty --border-width - The width of the select's borders, including the listbox.
* @cssproperty --box-shadow - The shadow effects around the edges of the select's combobox.
*
* @cssstate blank - The select is empty.
*/
@customElement('wa-select')
export default class WaSelect extends WebAwesomeFormAssociatedElement {
@@ -97,7 +99,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
return [...super.validators, ...validators];
}
assumeInteractionOn = ['blur', 'input'];
assumeInteractionOn = ['wa-blur', 'wa-input'];
private readonly hasSlotController = new HasSlotController(this, 'hint', 'label');
private readonly localize = new LocalizeController(this);
@@ -308,6 +310,11 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
private handleFocus() {
this.displayInput.setSelectionRange(0, 0);
this.dispatchEvent(new WaFocusEvent());
}
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleDocumentFocusIn = (event: KeyboardEvent) => {
@@ -359,8 +366,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Emit after updating
this.updateComplete.then(() => {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaInputEvent());
this.dispatchEvent(new WaChangeEvent());
});
if (!this.multiple) {
@@ -489,8 +496,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Emit after update
this.updateComplete.then(() => {
this.dispatchEvent(new WaClearEvent());
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaInputEvent());
this.dispatchEvent(new WaChangeEvent());
});
}
}
@@ -520,8 +527,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.value !== oldValue) {
// Emit after updating
this.updateComplete.then(() => {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaInputEvent());
this.dispatchEvent(new WaChangeEvent());
});
}
@@ -558,8 +565,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Emit after updating
this.updateComplete.then(() => {
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaInputEvent());
this.dispatchEvent(new WaChangeEvent());
});
}
}
@@ -667,14 +674,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
});
}
updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
}
}
@watch('disabled', { waitUntilFirstUpdate: true })
handleDisabledChange() {
// Close the listbox when the control is disabled
@@ -800,7 +799,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
>
<label
id="label"
part="form-control-label label"
part="form-control-label"
class="label"
aria-hidden=${hasLabel ? 'false' : 'true'}
@click=${this.handleLabelClick}
@@ -860,6 +859,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
role="combobox"
tabindex="0"
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
<!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. -->

View File

@@ -50,13 +50,13 @@ describe('<wa-slider>', () => {
});
describe('when the value changes', () => {
it('should emit change and input and decrease the value when pressing right arrow', async () => {
it('should emit wa-change and wa-input and decrease the value when pressing right arrow', async () => {
const el = await fixture<WaSlider>(html` <wa-slider value="50"></wa-slider> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await sendKeys({ press: 'ArrowRight' });
await el.updateComplete;
@@ -66,30 +66,30 @@ describe('<wa-slider>', () => {
expect(inputHandler).to.have.been.calledOnce;
});
it('should not emit change or input when changing the value programmatically', async () => {
it('should not emit wa-change or wa-input when changing the value programmatically', async () => {
const el = await fixture<WaSlider>(html` <wa-slider value="0"></wa-slider> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.value = 50;
await el.updateComplete;
});
it('should not emit change or input when stepUp() is called programmatically', async () => {
it('should not emit wa-change or wa-input when stepUp() is called programmatically', async () => {
const el = await fixture<WaSlider>(html` <wa-slider step="2" value="2"></wa-slider> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepUp();
await el.updateComplete;
});
it('should not emit change or input when stepDown() is called programmatically', async () => {
it('should not emit wa-change or wa-input when stepDown() is called programmatically', async () => {
const el = await fixture<WaSlider>(html` <wa-slider step="2" value="2"></wa-slider> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.stepDown();
await el.updateComplete;
});

View File

@@ -3,10 +3,14 @@ import { customElement, eventOptions, property, query, state } from 'lit/decorat
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { HasSlotController } from '../../internal/slot.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import sliderStyles from '../../styles/native/slider.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import { LocalizeController } from '../../utilities/localize.js';
@@ -21,10 +25,10 @@ import styles from './slider.css';
* @slot label - The slider label. Alternatively, you can use the `label` attribute.
* @slot hint - Text that describes how to use the input. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event change - Emitted when an alteration to the control's value is committed by the user.
* @event focus - Emitted when the control gains focus.
* @event input - Emitted when the control receives input.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-change - Emitted when an alteration to the control's value is committed by the user.
* @event wa-focus - Emitted when the control gains focus.
* @event wa-input - Emitted when the control receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart form-control - The form control that wraps the label, input, and hint.
@@ -156,21 +160,24 @@ export default class WaSlider extends WebAwesomeFormAssociatedElement {
this.resizeObserver?.unobserve(this.input);
}
private handleChange(event: Event) {
this.dispatchComposedEvent(event);
private handleChange() {
this.dispatchEvent(new WaChangeEvent());
}
private handleInput() {
this.value = parseFloat(this.input.value);
this.dispatchEvent(new WaInputEvent());
this.syncRange();
}
private handleBlur() {
this.hasTooltip = false;
this.dispatchEvent(new WaBlurEvent());
}
private handleFocus() {
this.hasTooltip = true;
this.dispatchEvent(new WaFocusEvent());
}
@eventOptions({ passive: true })

View File

@@ -49,13 +49,13 @@ describe('<wa-switch>', () => {
expect(el.checkValidity()).to.be.true;
});
it('should emit change and input when clicked', async () => {
it('should emit wa-change and wa-input when clicked', async () => {
const el = await fixture<WaSwitch>(html` <wa-switch></wa-switch> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.click();
await el.updateComplete;
@@ -64,13 +64,13 @@ describe('<wa-switch>', () => {
expect(el.checked).to.be.true;
});
it('should emit change when toggled with spacebar', async () => {
it('should emit wa-change when toggled with spacebar', async () => {
const el = await fixture<WaSwitch>(html` <wa-switch></wa-switch> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await sendKeys({ press: ' ' });
@@ -79,13 +79,13 @@ describe('<wa-switch>', () => {
expect(el.checked).to.be.true;
});
it('should emit change and input when toggled with the right arrow', async () => {
it('should emit wa-change and wa-input when toggled with the right arrow', async () => {
const el = await fixture<WaSwitch>(html` <wa-switch></wa-switch> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await sendKeys({ press: 'ArrowRight' });
await el.updateComplete;
@@ -95,13 +95,13 @@ describe('<wa-switch>', () => {
expect(el.checked).to.be.true;
});
it('should emit change and input when toggled with the left arrow', async () => {
it('should emit wa-change and wa-input when toggled with the left arrow', async () => {
const el = await fixture<WaSwitch>(html` <wa-switch checked></wa-switch> `);
const changeHandler = sinon.spy();
const inputHandler = sinon.spy();
el.addEventListener('change', changeHandler);
el.addEventListener('input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.focus();
await sendKeys({ press: 'ArrowLeft' });
await el.updateComplete;
@@ -111,10 +111,10 @@ describe('<wa-switch>', () => {
expect(el.checked).to.be.false;
});
it('should not emit change or input when checked is set by JavaScript', async () => {
it('should not emit wa-change or wa-input when checked is set by JavaScript', async () => {
const el = await fixture<WaSwitch>(html` <wa-switch></wa-switch> `);
el.addEventListener('change', () => expect.fail('change incorrectly emitted'));
el.addEventListener('input', () => expect.fail('input incorrectly emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change incorrectly emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-change incorrectly emitted'));
el.checked = true;
await el.updateComplete;
el.checked = false;

View File

@@ -4,10 +4,14 @@ import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { HasSlotController } from '../../internal/slot.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import formControlStyles from '../../styles/shadow/form-control.css';
import sizeStyles from '../../styles/utilities/size.css';
import styles from './switch.css';
@@ -21,10 +25,10 @@ import styles from './switch.css';
* @slot - The switch's label.
* @slot hint - Text that describes how to use the switch. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event change - Emitted when the control's checked state changes.
* @event input - Emitted when the control receives input.
* @event focus - Emitted when the control gains focus.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-change - Emitted when the control's checked state changes.
* @event wa-input - Emitted when the control receives input.
* @event wa-focus - Emitted when the control gains focus.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart base - The component's base wrapper.
@@ -113,25 +117,37 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
this.handleValueOrCheckedChange();
}
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
}
private handleInput() {
this.dispatchEvent(new WaInputEvent());
}
private handleClick() {
this.hasInteracted = true;
this.checked = !this.checked;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new WaChangeEvent());
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
private handleKeyDown(event: KeyboardEvent) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
this.checked = false;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
if (event.key === 'ArrowRight') {
event.preventDefault();
this.checked = true;
this.dispatchEvent(new Event('change'));
this.dispatchEvent(new InputEvent('input'));
this.dispatchEvent(new WaChangeEvent());
this.dispatchEvent(new WaInputEvent());
}
}
@@ -234,6 +250,9 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
aria-checked=${this.checked ? 'true' : 'false'}
aria-describedby="hint"
@click=${this.handleClick}
@input=${this.handleInput}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@keydown=${this.handleKeyDown}
/>

View File

@@ -90,22 +90,17 @@ export default class WaTabGroup extends WebAwesomeElement {
setTimeout(() => this.setAriaLabels());
}
// Only process mutations for elements that are children of this tab group
const relevantMutations = mutations.filter(m => {
const target = m.target as HTMLElement;
return target.closest('wa-tab-group') === this;
});
// Sync tabs when disabled states change
if (relevantMutations.some(m => m.attributeName === 'disabled')) {
if (mutations.some(m => m.attributeName === 'disabled')) {
this.syncTabsAndPanels();
} else if (relevantMutations.some(m => m.attributeName === 'active')) {
const tabs = relevantMutations
// sync tabs when active state on tab changes
} else if (mutations.some(m => m.attributeName === 'active')) {
const tabs = mutations
.filter(m => m.attributeName === 'active' && (m.target as HTMLElement).tagName.toLowerCase() === 'wa-tab')
.map(m => m.target as WaTab);
const newActiveTab = tabs.find(tab => tab.active);
if (newActiveTab && newActiveTab.closest('wa-tab-group') === this) {
if (newActiveTab) {
this.setActiveTab(newActiveTab);
}
}
@@ -169,7 +164,7 @@ export default class WaTabGroup extends WebAwesomeElement {
const tab = target.closest('wa-tab');
const tabGroup = tab?.closest('wa-tab-group');
// Ensure the target tab is in this specific tab group instance
// Ensure the target tab is in this tab group
if (tabGroup !== this) {
return;
}
@@ -184,7 +179,7 @@ export default class WaTabGroup extends WebAwesomeElement {
const tab = target.closest('wa-tab');
const tabGroup = tab?.closest('wa-tab-group');
// Ensure the target tab is in this specific tab group instance
// Ensure the target tab is in this tab group
if (tabGroup !== this) {
return;
}
@@ -302,11 +297,6 @@ export default class WaTabGroup extends WebAwesomeElement {
...options,
};
// Ensure the tab belongs to this tab group before activating
if (tab.closest('wa-tab-group') !== this) {
return;
}
if (tab !== this.activeTab && !tab.disabled) {
const previousTab = this.activeTab;
this.active = tab.panel;

View File

@@ -64,7 +64,7 @@ describe('<wa-textarea>', () => {
const label = el.shadowRoot!.querySelector('[part~="label"]')!;
const submitHandler = sinon.spy();
el.addEventListener('focus', submitHandler);
el.addEventListener('wa-focus', submitHandler);
(label as HTMLLabelElement).click();
await waitUntil(() => submitHandler.calledOnce);
@@ -72,13 +72,13 @@ describe('<wa-textarea>', () => {
});
describe('when the value changes', () => {
it('should emit change and input when the user types in the textarea', async () => {
it('should emit wa-change and wa-input when the user types in the textarea', async () => {
const el = await fixture<WaTextarea>(html` <wa-textarea></wa-textarea> `);
const inputHandler = sinon.spy();
const changeHandler = sinon.spy();
el.addEventListener('input', inputHandler);
el.addEventListener('change', changeHandler);
el.addEventListener('wa-input', inputHandler);
el.addEventListener('wa-change', changeHandler);
el.focus();
await sendKeys({ type: 'abc' });
el.blur();
@@ -88,21 +88,21 @@ describe('<wa-textarea>', () => {
expect(inputHandler).to.have.been.calledThrice;
});
it('should not emit change or input when the value is set programmatically', async () => {
it('should not emit wa-change or wa-input when the value is set programmatically', async () => {
const el = await fixture<WaTextarea>(html` <wa-textarea></wa-textarea> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.value = 'abc';
await el.updateComplete;
});
it('should not emit change or input when calling setRangeText()', async () => {
it('should not emit wa-change or wa-input when calling setRangeText()', async () => {
const el = await fixture<WaTextarea>(html` <wa-textarea value="hi there"></wa-textarea> `);
el.addEventListener('change', () => expect.fail('change should not be emitted'));
el.addEventListener('input', () => expect.fail('input should not be emitted'));
el.addEventListener('wa-change', () => expect.fail('wa-change should not be emitted'));
el.addEventListener('wa-input', () => expect.fail('wa-input should not be emitted'));
el.focus();
el.setSelectionRange(0, 2);
el.setRangeText('hello');

View File

@@ -4,10 +4,14 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaChangeEvent } from '../../events/change.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInputEvent } from '../../events/input.js';
import { HasSlotController } from '../../internal/slot.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/input.css';
import formControlStyles from '../../styles/shadow/form-control.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
@@ -23,10 +27,10 @@ import styles from './textarea.css';
* @slot label - The textarea's label. Alternatively, you can use the `label` attribute.
* @slot hint - Text that describes how to use the input. Alternatively, you can use the `hint` attribute.
*
* @event blur - Emitted when the control loses focus.
* @event change - Emitted when an alteration to the control's value is committed by the user.
* @event focus - Emitted when the control gains focus.
* @event input - Emitted when the control receives input.
* @event wa-blur - Emitted when the control loses focus.
* @event wa-change - Emitted when an alteration to the control's value is committed by the user.
* @event wa-focus - Emitted when the control gains focus.
* @event wa-input - Emitted when the control receives input.
* @event wa-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied.
*
* @csspart label - The label
@@ -39,8 +43,6 @@ import styles from './textarea.css';
* @cssproperty --border-color - The color of the textarea's borders.
* @cssproperty --border-width - The width of the textarea's borders.
* @cssproperty --box-shadow - The shadow effects around the edges of the textarea.
*
* @cssstate blank - The textarea is empty.
*/
@customElement('wa-textarea')
export default class WaTextarea extends WebAwesomeFormAssociatedElement {
@@ -50,7 +52,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
return [...super.validators, MirrorValidator()];
}
assumeInteractionOn = ['blur', 'input'];
assumeInteractionOn = ['wa-blur', 'wa-input'];
private readonly hasSlotController = new HasSlotController(this, 'hint', 'label');
private resizeObserver: ResizeObserver;
@@ -200,20 +202,26 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
}
private handleBlur() {
this.dispatchEvent(new WaBlurEvent());
this.checkValidity();
}
private handleChange(event: Event) {
private handleChange() {
this.valueHasChanged = true;
this.value = this.input.value;
this.setTextareaDimensions();
this.dispatchComposedEvent(event);
this.dispatchEvent(new WaChangeEvent());
this.checkValidity();
}
private handleFocus() {
this.dispatchEvent(new WaFocusEvent());
}
private handleInput() {
this.valueHasChanged = true;
this.value = this.input.value;
this.dispatchEvent(new WaInputEvent());
}
private setTextareaDimensions() {
@@ -267,12 +275,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
if (changedProperties.has('resize')) {
this.setTextareaDimensions();
}
super.updated(changedProperties);
if (changedProperties.has('value')) {
this.toggleCustomState('blank', !this.value);
}
}
/** Sets focus on the textarea. */
@@ -372,6 +375,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
aria-describedby="hint"
@change=${this.handleChange}
@input=${this.handleInput}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
></textarea>

View File

@@ -30,16 +30,14 @@ slot:not([name])::slotted(wa-icon) {
}
.checkbox {
line-height: var(--wa-form-control-value-line-height);
pointer-events: none;
}
.expand-button,
.checkbox,
.label {
font-family: inherit;
font: inherit;
font-size: var(--wa-font-size-m);
font-weight: inherit;
}
.checkbox::part(base) {

Some files were not shown because too many files have changed in this diff Show More