Merge branch 'next' into patterns-exploration

This commit is contained in:
lindsaym-fa
2024-01-26 10:17:43 -05:00
19 changed files with 277 additions and 143 deletions

View File

@@ -173,6 +173,7 @@
"valpha",
"valuenow",
"valuetext",
"Vuejs",
"WCAG",
"webawesome",
"WEBP",

View File

@@ -1,6 +1,7 @@
import * as path from 'path';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import { parse } from 'comment-parser';
import { pascalCase } from 'pascal-case';
import commandLineArgs from 'command-line-args';
@@ -218,6 +219,12 @@ export default {
url: `https://shoelace.style/components/${tag.replace('wa-', '')}`
};
}
}),
customElementVuejsPlugin({
outdir: './dist/types/vue',
fileName: 'index.d.ts',
componentTypePath: (_, tag) => `../../components/${tag.replace('wa-', '')}/${tag.replace('wa-', '')}.component.js`
})
]
};

View File

@@ -374,7 +374,7 @@ const App = () => (
### Loading
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. Clicks will be suppressed until the loading state is removed.
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around.
```html:preview
<wa-button variant="brand" loading>Brand</wa-button>

View File

@@ -35,35 +35,22 @@ If you'd rather not use the CDN for assets, you can create a build task that cop
## Configuration
You'll need to tell Vue to ignore Web Awesome components. This is pretty easy because they all start with `sl-`.
```js
import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-')
}
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
});
```
If you haven't configured your Vue.js project to work with custom elements/web components, follow [the instructions here](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue) based on your project type to ensure your project will not throw an error when it encounters a custom element.
Now you can start using Web Awesome components in your app!
## Types
Once you have configured your application for custom elements, you should be able to use Shoelace in your application without it causing any errors. Unfortunately, this doesn't register the custom elements to behave like components built using Vue. To provide autocomplete information and type safety for your components, you can import the Shoelace Vue types into your `tsconfig.json` to get better integration in your standard Vue and JSX templates.
```json
{
"compilerOptions": {
"types": ["@shoelace-style/shoelace/dist/types/vue"]
}
}
```
## Usage
### QR code generator example
@@ -126,7 +113,7 @@ Are you using Web Awesome with Vue? [Help us improve this page!](https://github.
### Slots
To use Web Awesome components with slots, follow the Vue documentation on using [slots with custom elements](https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue).
Slots in Web Awesome (and web components in general) are functionally the same as basic slots in Vue. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
Here is an example:

View File

@@ -26,12 +26,18 @@ New versions of Web Awesome are released as-needed and generally occur when a cr
## Next
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
- Added the `loading` attribute and the `spinner` and `spinner__base` part to `<sl-menu-item>` [#1700]
- Fixed files that did not have `.js` extensions. [#1770]
- Fixed `<sl-dialog>` not accounting for elements with hidden dialog controls like `<video>` [#1755]
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it work differently from `<input>` and `<textarea>` when using defaults [#1746]
- Fixed a bug in `<sl-select>` that prevented it from closing when tabbing to another select inside a shadow root [#1763]
- Fixed a bug in `<sl-spinner>` that caused the animation to appear strange in certain circumstances [#1787]
- Fixed a bug that caused form controls to submit even after they were removed from the DOM [#1823]
- Fixed a bug that caused empty `<sl-radio-group>` elements to log an error in the console [#1795]
- Fixed a bug that caused modal scroll locking to conflict with the `scrollbar-gutter` property [#1805]
- Fixed a bug in `<sl-option>` that caused slotted content to show up when calling `getTextLabel()` [#1730]
- Improved the accessibility of `<sl-tooltip>` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734]
## 2.12.0

43
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"": {
"name": "@shoelace-style/shoelace",
"version": "2.8.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.0.2",
@@ -39,6 +40,7 @@
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
"custom-element-vuejs-integration": "^1.0.0",
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "^0.19.4",
@@ -6760,6 +6762,30 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/custom-element-vuejs-integration": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-element-vuejs-integration/-/custom-element-vuejs-integration-1.0.1.tgz",
"integrity": "sha512-whoB5DqPNIxaltlvTuOXrP543o5dHKV1ae3a3qFHwKKKwDSCU9vtTOIZpZ4NdRmBPDbaCOgQvYxCJmjdDXrC+g==",
"dev": true,
"dependencies": {
"prettier": "^2.7.1"
}
},
"node_modules/custom-element-vuejs-integration/node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/custom-elements-manifest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",
@@ -23702,6 +23728,23 @@
}
}
},
"custom-element-vuejs-integration": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/custom-element-vuejs-integration/-/custom-element-vuejs-integration-1.0.1.tgz",
"integrity": "sha512-whoB5DqPNIxaltlvTuOXrP543o5dHKV1ae3a3qFHwKKKwDSCU9vtTOIZpZ4NdRmBPDbaCOgQvYxCJmjdDXrC+g==",
"dev": true,
"requires": {
"prettier": "^2.7.1"
},
"dependencies": {
"prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true
}
}
},
"custom-elements-manifest": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz",

View File

@@ -45,6 +45,7 @@
"start": "node scripts/build.js --serve",
"build": "node scripts/build.js",
"verify": "npm run prettier:check && npm run lint && npm run build && npm run test",
"postinstall": "npx playwright install",
"prepublishOnly": "npm run verify",
"prettier": "prettier --write --log-level warn .",
"prettier:check": "prettier --check --log-level warn .",
@@ -92,6 +93,7 @@
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
"custom-element-vuejs-integration": "^1.0.0",
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "^0.19.4",

View File

@@ -14,6 +14,13 @@ import type { CSSResultGroup } from 'lit';
* @slot - The badge's content.
*
* @csspart base - The component's base wrapper.
*
* @cssproperty --background - The badge's background styles.
* @cssproperty --border-color - The color of the badge's border.
* @cssproperty --border-radius - The radius of the badge's corners.
* @cssproperty --border-style - The style of the badge's border.
* @cssproperty --border-width - The width of the badge's border.
* @cssproperty --content-color - The color of the badge's content.
*/
export default class WaBadge extends WebAwesomeElement {
static styles: CSSResultGroup = styles;

View File

@@ -5,9 +5,39 @@ export default css`
${componentStyles}
:host {
--border-color: var(--wa-color-surface-default);
--border-radius: var(--wa-corners-xs);
--border-style: var(--wa-border-style);
--border-width: var(--wa-border-width-s);
display: inline-flex;
}
:host([variant='brand']) {
--background: var(--wa-color-brand-spot);
--content-color: var(--wa-color-brand-text-on-spot);
}
:host([variant='success']) {
--background: var(--wa-color-success-spot);
--content-color: var(--wa-color-success-text-on-spot);
}
:host([variant='warning']) {
--background: var(--wa-color-warning-spot);
--content-color: var(--wa-color-warning-text-on-spot);
}
:host([variant='neutral']) {
--background: var(--wa-color-neutral-spot);
--content-color: var(--wa-color-neutral-text-on-spot);
}
:host([variant='danger']) {
--background: var(--wa-color-danger-spot);
--content-color: var(--wa-color-danger-text-on-spot);
}
.badge {
display: inline-flex;
align-items: center;
@@ -15,8 +45,12 @@ export default css`
font-size: max(12px, 0.75em);
font-weight: var(--wa-font-weight-medium);
line-height: 1;
border-radius: var(--wa-corners-xs);
border: solid 1px var(--wa-color-surface-default);
background: var(--background);
border-color: var(--border-color);
border-radius: var(--border-radius);
border-style: var(--border-style);
border-width: var(--border-width);
color: var(--content-color);
white-space: nowrap;
padding: 0.35em 0.6em;
user-select: none;
@@ -24,32 +58,6 @@ export default css`
cursor: inherit;
}
/* Variant modifiers */
.badge--brand {
background-color: var(--wa-color-brand-spot);
color: var(--wa-color-brand-text-on-spot);
}
.badge--success {
background-color: var(--wa-color-success-spot);
color: var(--wa-color-success-text-on-spot);
}
.badge--neutral {
background-color: var(--wa-color-neutral-spot);
color: var(--wa-color-neutral-text-on-spot);
}
.badge--warning {
background-color: var(--wa-color-warning-spot);
color: var(--wa-color-warning-text-on-spot);
}
.badge--danger {
background-color: var(--wa-color-danger-spot);
color: var(--wa-color-danger-text-on-spot);
}
/* Pill modifier */
.badge--pill {
border-radius: var(--wa-corners-pill);

View File

@@ -41,10 +41,13 @@ import type WaCarouselItem from '../carousel-item/carousel-item.component.js';
* @csspart navigation-button--previous - Applied to the previous button.
* @csspart navigation-button--next - Applied to the next button.
*
* @cssproperty --slide-gap - The space between each slide.
* @cssproperty [--aspect-ratio=16/9] - The aspect ratio of each slide.
* @cssproperty --navigation-color - The color of the navigation buttons.
* @cssproperty --pagination-color-activated - The color of the pagination dot for the current item.
* @cssproperty --pagination-color-resting - The color of the pagination dots for inactive items.
* @cssproperty --scroll-hint - The amount of padding to apply to the scroll area, allowing adjacent slides to become
* partially visible as a scroll hint.
* @cssproperty --slide-gap - The space between each slide.
*/
export default class WaCarousel extends WebAwesomeElement {
static styles: CSSResultGroup = styles;

View File

@@ -5,9 +5,12 @@ export default css`
${componentStyles}
:host {
--slide-gap: var(--wa-space-m, 1rem);
--aspect-ratio: 16 / 9;
--navigation-color: var(--wa-color-text-quiet);
--pagination-color-activated: var(--wa-form-controls-activated-color);
--pagination-color-resting: var(--wa-form-controls-resting-color);
--scroll-hint: 0px;
--slide-gap: var(--wa-space-m, 1rem);
display: flex;
}
@@ -95,7 +98,7 @@ export default css`
.carousel__navigation {
grid-area: navigation;
display: contents;
font-size: var(--wa-font-size-xl);
font-size: var(--wa-font-size-l);
}
.carousel__navigation-button {
@@ -106,7 +109,7 @@ export default css`
border: none;
border-radius: var(--wa-corners-s);
font-size: inherit;
color: var(--wa-color-text-quiet);
color: var(--navigation-color);
padding: var(--wa-space-xs);
cursor: pointer;
transition: var(--wa-transition-fast) color;
@@ -140,14 +143,15 @@ export default css`
border-radius: var(--wa-corners-circle);
width: var(--wa-space-s);
height: var(--wa-space-s);
background-color: var(--wa-color-neutral-fill-highlight);
background-color: var(--pagination-color-resting);
padding: 0;
margin: 0;
transition: transform var(--wa-transition-normal);
}
.carousel__pagination-item--active {
background-color: var(--wa-color-brand-spot);
transform: scale(1.2);
background-color: var(--pagination-color-activated);
transform: scale(1.25);
}
/* Focus styles */

View File

@@ -667,7 +667,7 @@ export default class WaColorPicker extends WebAwesomeElement implements WebAweso
/** Generates a hex string from HSV values. Hue must be 0-360. All other arguments must be 0-100. */
private getHexString(hue: number, saturation: number, brightness: number, alpha = 100) {
const color = new TinyColor(`hsva(${hue}, ${saturation}, ${brightness}, ${alpha / 100})`);
const color = new TinyColor(`hsva(${hue}, ${saturation}%, ${brightness}%, ${alpha / 100})`);
if (!color.isValid) {
return '';
}

View File

@@ -106,7 +106,22 @@ export default class WaOption extends WebAwesomeElement {
/** Returns a plain text label based on the option's content. */
getTextLabel() {
return (this.textContent ?? '').trim();
const nodes = this.childNodes;
let label = '';
[...nodes].forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (!(node as HTMLElement).hasAttribute('slot')) {
label += (node as HTMLElement).outerHTML;
}
}
if (node.nodeType === Node.TEXT_NODE) {
label += node.textContent;
}
});
return label.trim();
}
render() {

View File

@@ -28,6 +28,7 @@ import type WaRadioButton from '../radio-button/radio-button.js';
* @slot - The default slot where `<wa-radio>` or `<wa-radio-button>` elements are placed.
* @slot label - The radio group's label. Required for proper accessibility. Alternatively, you can use the `label`
* attribute.
* @slot help-text - Text that describes how to use the radio group. Alternatively, you can use the `help-text` attribute.
*
* @event wa-change - Emitted when the radio group's selected value changes.
* @event wa-input - Emitted when the radio group receives user input.
@@ -218,7 +219,7 @@ export default class WaRadioGroup extends WebAwesomeElement implements WebAwesom
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
if (!radios.some(radio => radio.checked)) {
if (radios.length > 0 && !radios.some(radio => radio.checked)) {
if (this.hasButtonGroup) {
const buttonRadio = radios[0].shadowRoot?.querySelector('button');

View File

@@ -23,6 +23,13 @@ import type { CSSResultGroup } from 'lit';
* @csspart content - The tag's content.
* @csspart remove-button - The tag's remove button, an `<wa-icon-button>`.
* @csspart remove-button__base - The remove button's exported `base` part.
*
* @cssproperty --background - The tag's background styles.
* @cssproperty --border-color - The color of the tag's border.
* @cssproperty --border-radius - The radius of the tag's corners.
* @cssproperty --border-style - The style of the tag's border.
* @cssproperty --border-width - The width of the tag's border.
* @cssproperty --content-color - The color of the tag's content.
*/
export default class WaTag extends WebAwesomeElement {
static styles: CSSResultGroup = styles;

View File

@@ -5,13 +5,52 @@ export default css`
${componentStyles}
:host {
--border-radius: var(--wa-corners-xs);
--border-style: var(--wa-border-style);
--border-width: var(--wa-border-width-s);
display: inline-block;
}
:host([variant='brand']) {
--background: var(--wa-color-brand-fill-subtle);
--border-color: var(--wa-color-brand-border-highlight);
--content-color: var(--wa-color-brand-text-on-fill);
}
:host([variant='success']) {
--background: var(--wa-color-success-fill-subtle);
--border-color: var(--wa-color-success-border-highlight);
--content-color: var(--wa-color-success-text-on-fill);
}
:host([variant='warning']) {
--background: var(--wa-color-warning-fill-subtle);
--border-color: var(--wa-color-warning-border-highlight);
--content-color: var(--wa-color-warning-text-on-fill);
}
:host([variant='neutral']) {
--background: var(--wa-color-neutral-fill-subtle);
--border-color: var(--wa-color-neutral-border-highlight);
--content-color: var(--wa-color-neutral-text-on-fill);
}
:host([variant='danger']) {
--background: var(--wa-color-danger-fill-subtle);
--border-color: var(--wa-color-danger-border-highlight);
--content-color: var(--wa-color-danger-text-on-fill);
}
.tag {
display: flex;
align-items: center;
border: solid var(--wa-border-width-s);
background: var(--background);
border-color: var(--border-color);
border-radius: var(--border-radius);
border-style: var(--border-style);
border-width: var(--border-width);
color: var(--content-color);
line-height: 1;
white-space: nowrap;
user-select: none;
@@ -23,82 +62,14 @@ export default css`
padding: 0;
}
/*
* Variant modifiers
*/
.tag--brand {
background-color: var(--wa-color-brand-fill-subtle);
border-color: var(--wa-color-brand-border-highlight);
color: var(--wa-color-brand-text-on-fill);
}
.tag--brand:active > wa-icon-button {
color: var(--wa-color-brand-text-on-fill);
}
.tag--success {
background-color: var(--wa-color-success-fill-subtle);
border-color: var(--wa-color-success-border-highlight);
color: var(--wa-color-success-text-on-fill);
}
.tag--success:active > wa-icon-button {
color: var(--wa-color-success-text-on-fill);
}
.tag--neutral {
background-color: var(--wa-color-neutral-fill-subtle);
border-color: var(--wa-color-neutral-border-highlight);
color: var(--wa-color-neutral-text-on-fill);
}
.tag--neutral:active > wa-icon-button {
color: var(--wa-color-neutral-text-on-fill);
}
.tag--warning {
background-color: var(--wa-color-warning-fill-subtle);
border-color: var(--wa-color-warning-border-highlight);
color: var(--wa-color-warning-text-on-fill);
}
.tag--warning:active > wa-icon-button {
color: var(--wa-color-warning-text-on-fill);
}
.tag--danger {
background-color: var(--wa-color-danger-fill-subtle);
border-color: var(--wa-color-danger-border-highlight);
color: var(--wa-color-danger-text-on-fill);
}
.tag--danger:active > wa-icon-button {
color: var(--wa-color-danger-text-on-fill);
.tag:active > wa-icon-button {
color: var(--content-color);
}
/*
* Size modifiers
*/
.tag--small {
font-size: var(--wa-font-size-xs);
border-radius: var(--wa-corners-s);
padding: var(--wa-space-3xs) var(--wa-space-2xs);
}
.tag--medium {
font-size: var(--wa-font-size-s);
border-radius: var(--wa-corners-s);
padding: var(--wa-space-2xs) var(--wa-space-xs);
}
.tag--large {
font-size: var(--wa-font-size-m);
border-radius: var(--wa-corners-s);
padding: var(--wa-space-2xs) var(--wa-space-xs);
}
.tag--small {
font-size: var(--wa-font-size-xs);
height: calc(var(--wa-form-controls-height-s) * 0.8);

View File

@@ -1,6 +1,6 @@
import '../../../dist/webawesome.js';
import { expect, fixture, html } from '@open-wc/testing';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
// Reproduction of this issue: https://github.com/shoelace-style/shoelace/issues/1703
it('Should still run form validations if an element is removed', async () => {
@@ -19,3 +19,59 @@ it('Should still run form validations if an element is removed', async () => {
expect(form.checkValidity()).to.equal(false);
expect(form.reportValidity()).to.equal(false);
});
it('should submit the correct form values', async () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<wa-input name="a" value="1"></wa-input>
<wa-input name="b" value="2"></wa-input>
<wa-input name="c" value="3"></wa-input>
<wa-button type="submit">Submit</wa-button>
</form>
`);
const button = form.querySelector('wa-button')!;
const submitHandler = sinon.spy((event: SubmitEvent) => {
formData = new FormData(form);
event.preventDefault();
});
let formData: FormData;
form.addEventListener('submit', submitHandler);
button.click();
await waitUntil(() => submitHandler.calledOnce);
expect(formData!.get('a')).to.equal('1');
expect(formData!.get('b')).to.equal('2');
expect(formData!.get('c')).to.equal('3');
});
it('should submit the correct form values when form controls are removed from the DOM', async () => {
const form = await fixture<HTMLFormElement>(html`
<form>
<wa-input name="a" value="1"></wa-input>
<wa-input name="b" value="2"></wa-input>
<wa-input name="c" value="3"></wa-input>
<wa-button type="submit">Submit</wa-button>
</form>
`);
const button = form.querySelector('wa-button')!;
const submitHandler = sinon.spy((event: SubmitEvent) => {
formData = new FormData(form);
event.preventDefault();
});
let formData: FormData;
form.addEventListener('submit', submitHandler);
form.querySelector('[name="b"]')!.remove();
button.click();
await waitUntil(() => submitHandler.calledOnce);
expect(formData!.get('a')).to.equal('1');
expect(formData!.get('b')).to.equal(null);
expect(formData!.get('c')).to.equal('3');
});

View File

@@ -217,7 +217,14 @@ export class FormControlController implements ReactiveController {
// injecting the name/value on a temporary button, so we can just skip them here.
const isButton = this.host.tagName.toLowerCase() === 'wa-button';
if (!disabled && !isButton && typeof name === 'string' && name.length > 0 && typeof value !== 'undefined') {
if (
this.host.isConnected &&
!disabled &&
!isButton &&
typeof name === 'string' &&
name.length > 0 &&
typeof value !== 'undefined'
) {
if (Array.isArray(value)) {
(value as unknown[]).forEach(val => {
event.formData.append(name, (val as string | number | boolean).toString());

View File

@@ -4,9 +4,18 @@
* to reduce the possibility of collisions.
*/
.wa-scroll-lock {
padding-right: var(--wa-scroll-lock-size) !important;
overflow: hidden !important;
@supports (scrollbar-gutter: stable) {
.wa-scroll-lock {
scrollbar-gutter: stable !important;
overflow: hidden !important;
}
}
@supports not (scrollbar-gutter: stable) {
.wa-scroll-lock {
padding-right: var(--wa-scroll-lock-size) !important;
overflow: hidden !important;
}
}
.wa-toast-stack {