Compare commits

..

1 Commits

Author SHA1 Message Date
Lea Verou
3dc526c948 Support inheritance and initial values 2025-01-21 15:26:45 -05:00
70 changed files with 817 additions and 684 deletions

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,5 +1,3 @@
{%- 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>
@@ -7,18 +5,14 @@
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 %}
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
```
</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 %}
@import url('{% cdnUrl stylesheet %}');
```
</wa-tab-panel>
</wa-tab-group>

View File

@@ -3,15 +3,64 @@
{# {% set forceTheme = page.fileSlug %} #}
{% extends '../_includes/base.njk' %}
{% set themeId = page.fileSlug %}
{% if themeId == 'remixed' -%}
{% set themeId = 'default' %}
{% endif -%}
{% block header %}
<iframe src='/docs/themes/{{ themeId }}/demo.html' id="demo"></iframe>
<iframe src='{{ page.url }}demo.html' id="demo"></iframe>
<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="colors" label="Colors from…" size="small">
<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 %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}">{{ 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 %}
{% if p.fileSlug !== palette %}
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" size="small">
<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 %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
</p>
<script>
document.querySelector('#mix_and_match').addEventListener('wa-change', function(event) {
let selects = document.querySelectorAll('#mix_and_match wa-select');
let url = new URL(demo.src);
for (let select of selects) {
url.searchParams.set(select.name, select.value);
}
demo.src = url;
});
</script>
{% if palette -%}
<h2>Default Color Palette</h2>
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
{% set themePage = page %}
@@ -20,20 +69,34 @@
{% include 'page-card.njk' %}
</div>
{% set page = themePage %}
{% endif %}
{% 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' %}
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
{% include 'import-stylesheet-code.md.njk' %}
{% endif %}
### Remixing { #remixing }
If you want to combine the **colors** from this theme with another theme, you can import this CSS file *after* the other themes CSS file:
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/color.css' %}
{% include 'import-stylesheet-code.md.njk' %}
To use the **typography** from this theme with another theme, you can import this CSS file *after* the other themes CSS file:
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/typography.css' %}
{% include 'import-stylesheet-code.md.njk' %}
<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>
## Dark mode

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

@@ -28,7 +28,7 @@ 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);

View File

@@ -521,7 +521,7 @@ table.colors {
}
}
.page-wide {
.layout-theme {
wa-page > main {
max-width: 140ch;
@@ -529,13 +529,30 @@ table.colors {
max-width: 80ch;
}
}
}
.layout-theme {
iframe {
width: 100%;
min-height: 16lh;
height: 65vh;
max-height: 21lh;
}
#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

@@ -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

@@ -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

@@ -0,0 +1,69 @@
---
title: Inheritance & Default value tests
---
Button variant should default to `neutral`:
```html {.example}
<wa-button>Neutral</wa-button>
<wa-button variant="neutral">Neutral</wa-button>
<wa-button variant="brand">Brand</wa-button>
```
Callout variant should default to `brand`.
Buttons within an element with a variant should inherit that variant unless they have a variant of their own.
```html {.example}
<wa-callout>
Brand
<wa-button>Brand</wa-button>
<wa-button variant="neutral">Neutral</wa-button>
<wa-button variant="brand">Brand</wa-button>
<button>Brand</button>
<button class="wa-neutral">Neutral</button>
<button class="wa-brand">Brand</button>
</wa-callout>
<wa-callout variant="neutral">
Neutral
<wa-button>Neutral</wa-button>
<wa-button variant="neutral">Neutral</wa-button>
<wa-button variant="brand">Brand</wa-button>
<button>Neutral</button>
<button class="wa-neutral">Neutral</button>
<button class="wa-brand">Brand</button>
</wa-callout>
```
Nested callouts:
```html {.example}
<wa-callout>
Brand
<wa-callout>Brand</wa-callout>
</wa-callout>
<wa-callout variant="neutral">
Neutral
<wa-callout>Neutral</wa-callout>
</wa-callout>
```
```html {.example}
<wa-callout>
Brand
<wa-button>Brand</wa-button>
<wa-button variant="neutral">Neutral</wa-button>
<button>Brand</button>
<button class="wa-neutral">Neutral</button>
<br>
<br>
<wa-callout variant="neutral">
Neutral
<wa-button>Neutral</wa-button>
<wa-button variant="brand">Brand</wa-button>
<button>Neutral</button>
<button class="wa-brand">Brand</button>
</wa-callout>
</wa-callout>
```

View File

@@ -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 => {
@@ -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");
@@ -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

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

View File

@@ -14,11 +14,6 @@ During the alpha period, things might break! We take breaking changes very serio
## 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

View File

@@ -19,10 +19,6 @@ eleventyComputed:
{% 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>
@@ -43,12 +39,12 @@ function updateTheme() {
let params = new URLSearchParams(window.location.search);
let script = document.currentScript;
const stylesheetURLs = {
color: id => `/dist/styles/themes/${ id }/color.css`,
colors: 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',
colors: 'palette',
palette: 'swatchbook',
typography: 'font-case'
}
@@ -70,14 +66,11 @@ function updateTheme() {
}
}
let isRemixed = msgs.length > 0;
document.documentElement.classList.toggle("remixed", isRemixed);
for (let p of mix_and_match) {
p.hidden = !isRemixed;
if (isRemixed) {
p.hidden = msgs.length === 0;
if (msgs.length) {
let icon =
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong><br> ` + msgs.map(msg => `<wa-badge appearance=outlined>
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined>
${ msg }</wa-badge>`).join(' ');
}
}

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,18 +9,6 @@ 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);

View File

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

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

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.
@@ -67,7 +69,8 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@property() title = ''; // make reactive to pass through
/** The button's theme variant. */
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
@property({ reflect: true, initial: 'neutral' })
variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'inherit' = 'inherit';
/** The button's visual appearance. */
@property({ reflect: true, default: 'accent' })
@@ -140,6 +143,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 +275,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

@@ -28,7 +28,13 @@ export default class WaCallout extends WebAwesomeElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
/** The callout's theme variant. */
@property({ reflect: true }) variant: 'brand' | 'success' | 'neutral' | 'warning' | 'danger' = 'brand';
@property({ reflect: true, initial: 'brand' }) variant:
| 'brand'
| 'success'
| 'neutral'
| 'warning'
| 'danger'
| 'inherit' = 'inherit';
/** The callout's visual appearance. */
@property({ reflect: true }) appearance:

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')
@@ -232,6 +248,9 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
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>

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

@@ -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

@@ -3,11 +3,15 @@ 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
@@ -66,7 +70,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 +227,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 +242,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) {
@@ -432,6 +445,8 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
@change=${this.handleChange}
@input=${this.handleInput}
@keydown=${this.handleKeyDown}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
/>
${isClearIconVisible

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

@@ -5,8 +5,12 @@ 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.
@@ -97,7 +101,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 +312,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 +368,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 +498,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 +529,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 +567,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());
});
}
}
@@ -860,6 +869,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

@@ -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
@@ -50,7 +54,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 +204,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() {
@@ -372,6 +382,7 @@ export default class WaTextarea extends WebAwesomeFormAssociatedElement {
aria-describedby="hint"
@change=${this.handleChange}
@input=${this.handleInput}
@focus=${this.handleFocus}
@blur=${this.handleBlur}
></textarea>

11
src/events/blur.ts Normal file
View File

@@ -0,0 +1,11 @@
export class WaBlurEvent extends Event {
constructor() {
super('wa-blur', { bubbles: true, cancelable: false, composed: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-blur': WaBlurEvent;
}
}

11
src/events/change.ts Normal file
View File

@@ -0,0 +1,11 @@
export class WaChangeEvent extends Event {
constructor() {
super('wa-change', { bubbles: true, cancelable: false, composed: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-change': WaChangeEvent;
}
}

11
src/events/close.ts Normal file
View File

@@ -0,0 +1,11 @@
export class WaCloseEvent extends Event {
constructor() {
super('wa-close', { bubbles: true, cancelable: false, composed: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-close': WaCloseEvent;
}
}

View File

@@ -2,15 +2,20 @@ export type { WaAfterCollapseEvent } from './after-collapse.js';
export type { WaAfterExpandEvent } from './after-expand.js';
export type { WaAfterHideEvent } from './after-hide.js';
export type { WaAfterShowEvent } from './after-show.js';
export type { WaBlurEvent } from './blur.js';
export type { WaCancelEvent } from './cancel.js';
export type { WaChangeEvent } from './change.js';
export type { WaClearEvent } from './clear.js';
export type { WaCloseEvent } from './close.js';
export type { WaCollapseEvent } from './collapse.js';
export type { WaCopyEvent } from './copy.js';
export type { WaErrorEvent } from './error.js';
export type { WaExpandEvent } from './expand.js';
export type { WaFinishEvent } from './finish.js';
export type { WaFocusEvent } from './focus.js';
export type { WaHideEvent } from './hide.js';
export type { WaHoverEvent } from './hover.js';
export type { WaInputEvent } from './input.js';
export type { WaInvalidEvent } from './invalid.js';
export type { WaLazyChangeEvent } from './lazy-change.js';
export type { WaLazyLoadEvent } from './lazy-load.js';

11
src/events/focus.ts Normal file
View File

@@ -0,0 +1,11 @@
export class WaFocusEvent extends Event {
constructor() {
super('wa-focus', { bubbles: true, cancelable: false, composed: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-focus': WaFocusEvent;
}
}

11
src/events/input.ts Normal file
View File

@@ -0,0 +1,11 @@
export class WaInputEvent extends Event {
constructor() {
super('wa-input', { bubbles: true, cancelable: false, composed: true });
}
}
declare global {
interface GlobalEventHandlersEventMap {
'wa-input': WaInputEvent;
}
}

View File

@@ -2,7 +2,7 @@ import { aTimeout, expect } from '@open-wc/testing';
import { html, type TemplateResult } from 'lit';
import { html as staticHTML, unsafeStatic } from 'lit/static-html.js';
import { clickOnElement } from '../test.js';
import type { WebAwesomeFormControl } from '../webawesome-form-associated-element.js';
import type { WebAwesomeFormControl } from '../webawesome-formassociated-element.js';
import type { clientFixture, hydratedFixture } from './fixture.js';
import { fixtures } from './fixture.js';

View File

@@ -1,4 +1,4 @@
import type { Validator } from '../webawesome-form-associated-element.js';
import type { Validator } from '../webawesome-formassociated-element.js';
/**
* This validator is for if you have an exact copy of your element in the shadow DOM. Rather than needing

View File

@@ -1,4 +1,4 @@
import type { Validator } from '../webawesome-form-associated-element.js';
import type { Validator } from '../webawesome-formassociated-element.js';
/**
* This validator is for if you have an exact copy of your element in the shadow DOM. Rather than needing

View File

@@ -1,4 +1,4 @@
import type { Validator } from '../webawesome-form-associated-element.js';
import type { Validator } from '../webawesome-formassociated-element.js';
export interface RequiredValidatorOptions {
/** This is a cheap way for us to get translation strings for the user without having proper translations. */

View File

@@ -10,6 +10,7 @@ declare module 'lit' {
* Specifies the propertys default value
*/
default?: any;
initial?: any;
}
}
@@ -24,6 +25,13 @@ export default class WebAwesomeElement extends LitElement {
/* eslint-disable-next-line */
console.error('Element internals are not supported in your browser. Consider using a polyfill');
}
let Self = this.constructor as typeof WebAwesomeElement;
for (let [property, spec] of Self.elementProperties) {
if (spec.default === 'inherit' && spec.initial !== undefined && typeof property === 'string') {
this.toggleCustomState(`initial-${property}-${spec.initial}`);
}
}
}
// Make localization attributes reactive
@@ -159,48 +167,70 @@ export default class WebAwesomeElement extends LitElement {
return this.hasStatesSupport() ? this.internals.states.has(state) : false;
}
/**
* Given a native event, this function ensures it's composed and, if not, dispatches it again as a composed event.
* This is useful for relaying native events such as `change`, which will otherwise not be retargeted. It is safe,
* albeit sloppy, to call this on composed events, as it will no-op.
*/
dispatchComposedEvent(event: Event) {
if (!event.composed) {
this.dispatchEvent(new (event.constructor as typeof Event)(event.type, { ...event, composed: true }));
}
}
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
if (options && options.default !== undefined && options.converter === undefined) {
// Wrap the default converter to remove the attribute if the value is the default
// This effectively prevents the component sprouting attributes that have not been specified
let converter = {
...defaultConverter,
toAttribute(value: string, type: unknown): unknown {
if (value === options!.default) {
return null;
}
return defaultConverter.toAttribute!(value, type);
},
};
options = { ...options, converter };
if (options) {
if (options.initial !== undefined && options.default === undefined) {
// Set "inherit" value as default if no default is specified but the initial value is
// This saves us having to tediously specify default: "inherit", initial: "foo" for every property
options.default = 'inherit';
}
if (options.default !== undefined && options.converter === undefined) {
// Wrap the default converter to remove the attribute if the value is the default
// This effectively prevents the component sprouting attributes that have not been specified
let converter = {
...defaultConverter,
toAttribute(value: string, type: unknown): unknown {
if (value === options!.default) {
return null;
}
return defaultConverter.toAttribute!(value, type);
},
};
options = { ...options, converter };
}
}
super.createProperty(name, options);
// Wrap the default accessor with logic to return the default value if the value is null
if (options && options.default !== undefined) {
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name as string);
if (options) {
if (options.default !== undefined) {
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name as string);
if (descriptor?.get) {
const getter = descriptor.get;
if (descriptor?.get) {
const getter = descriptor.get;
Object.defineProperty(this.prototype, name, {
...descriptor,
get() {
return getter.call(this) ?? options.default;
},
});
Object.defineProperty(this.prototype, name, {
...descriptor,
get() {
return getter.call(this) ?? options.default;
},
});
}
if (options.default === 'inherit') {
// Add getter for "computed" value (taking ancestors into account)
let capitalizedName = name.toString().replace(/^\w/, c => c.toUpperCase());
Object.defineProperty(this.prototype, `computed${capitalizedName}`, {
get() {
let value;
let element = this;
do {
value = element[name as string];
element = element.parentElement;
} while (value === 'inherit' && element.parentElement);
if (value === 'inherit') {
// If we've reached this point and we still have `inherit`, we just ran out of parents
return options.initial;
}
return value;
},
});
}
}
}
}

View File

@@ -95,7 +95,7 @@ export class WebAwesomeFormAssociatedElement
required: boolean = false;
assumeInteractionOn: string[] = ['input'];
assumeInteractionOn: string[] = ['wa-input'];
// Additional
input?: (HTMLElement & { value: unknown }) | HTMLInputElement | HTMLTextAreaElement;
@@ -154,7 +154,7 @@ export class WebAwesomeFormAssociatedElement
}
if (changedProperties.has('value') || changedProperties.has('disabled')) {
// @ts-expect-error Some components will use an accessors, other use a property, so we don't want to limit them.
// @ts-expect-error Some components will use an accessors, other use a property, so we dont want to limit them.
const value = this.value as unknown;
// Accounts for the snowflake case on `<wa-select>`

View File

@@ -73,11 +73,19 @@ select:focus,
/* Replace native select caret */
select {
appearance: none;
--icon-caret: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path fill="rgb(180 180 200)" d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>');
background-image: var(--icon-caret), var(--icon-caret);
background-repeat: no-repeat;
background-position: center right var(--wa-space);
background-blend-mode: hue, difference;
}
label:has(select) {
position: relative;
--icon-caret: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>');
&::after {
content: '';
background-color: var(--wa-color-neutral-on-quiet);
mask: var(--icon-caret) 50% 50% no-repeat;
width: 1rem;
height: var(--wa-form-control-height);
position: absolute;
bottom: var(--wa-form-control-border-width);
right: var(--wa-space);
}
}