mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
11 Commits
card-slot-
...
spinner-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1791f27dda | ||
|
|
e2f2a8a0d3 | ||
|
|
86293cc3e1 | ||
|
|
8fb521d9ef | ||
|
|
2491ca45ac | ||
|
|
411d385d65 | ||
|
|
634828d89a | ||
|
|
17fd3d238c | ||
|
|
ab0f2a1411 | ||
|
|
4069360242 | ||
|
|
8c51c5b933 |
@@ -124,6 +124,22 @@ export default async function (eleventyConfig) {
|
||||
eleventyConfig.addFilter('stripExtension', string => path.parse(string + '').name);
|
||||
eleventyConfig.addFilter('stripPrefix', content => content.replace(/^wa-/, ''));
|
||||
eleventyConfig.addFilter('uniqueId', (_value, length = 8) => nanoid(length));
|
||||
|
||||
eleventyConfig.addGlobalData('eleventyComputed', {
|
||||
// Page title with smart + default site name formatting
|
||||
pageTitle: data => composePageTitle(data.title),
|
||||
// Open Graph title with smart + default site name formatting
|
||||
ogTitle: data => composePageTitle(data.ogTitle || data.title),
|
||||
ogDescription: data => data.ogDescription || data.description,
|
||||
ogImage: data => data.ogImage || siteMetadata.image,
|
||||
ogUrl: data => {
|
||||
if (data.ogUrl) return data.ogUrl;
|
||||
const url = data.page?.url || '';
|
||||
return url ? `${siteMetadata.url}${url}` : siteMetadata.url;
|
||||
},
|
||||
ogType: data => data.ogType || 'website',
|
||||
});
|
||||
|
||||
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
|
||||
// With Prettier 3, this means a leading pipe will exist be present when the line wraps.
|
||||
eleventyConfig.addFilter('trimPipes', content => {
|
||||
@@ -297,18 +313,19 @@ export default async function (eleventyConfig) {
|
||||
|
||||
// This needs to happen in "eleventy.after" otherwise incremental builds never update.
|
||||
eleventyConfig.on('eleventy.after', function () {
|
||||
let assetsDir = path.join(process.env.BASE_DIR || 'docs', 'assets');
|
||||
const baseDir = process.env.BASE_DIR || 'docs';
|
||||
let assetsDir = path.join(baseDir, 'assets');
|
||||
const siteAssetsDir = path.join(eleventyConfig.directories.output, 'assets');
|
||||
fs.cpSync(assetsDir, siteAssetsDir, { recursive: true });
|
||||
|
||||
// Passthrough copy for manifest.json (PWA manifest file)
|
||||
fs.cpSync(path.join(baseDir, 'manifest.json'), path.join(eleventyConfig.directories.output, 'manifest.json'));
|
||||
});
|
||||
|
||||
for (let glob of passThrough) {
|
||||
eleventyConfig.addPassthroughCopy(glob);
|
||||
}
|
||||
|
||||
// Passthrough copy for manifest.json (PWA manifest file)
|
||||
eleventyConfig.addPassthroughCopy('manifest.json');
|
||||
|
||||
// // SSR plugin
|
||||
// if (!isDev) {
|
||||
// //
|
||||
|
||||
@@ -38,7 +38,7 @@ wa-page > [slot='banner'] {
|
||||
}
|
||||
|
||||
&.banner-wa-launch {
|
||||
/* custom brand colors carrried over from theme-site for the banner */
|
||||
/* custom brand colors carried over from theme-site for the banner */
|
||||
--wa-color-brand-95: #fef0ec;
|
||||
--wa-color-brand-90: #fce0d8;
|
||||
--wa-color-brand-80: #f8bcac;
|
||||
|
||||
@@ -13,7 +13,12 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
|
||||
|
||||
## Next
|
||||
|
||||
- 🚨 BREAKING: Changed `appearance="filled outlined"` to `appearance="filled-outlined"` in `<wa-card>` [issue:1671]
|
||||
- Fixed a bug in `<wa-slider>` that caused some touch devices to end up with the incorrect value [issue:1703]
|
||||
- Fixed a bug in `<wa-card>` that prevented some slots from being detected correctly [discuss:1450]
|
||||
- Fixed a bug in `<wa-dropdown>` that caused the browser to hang when cancelling the `wa-hide` event [issue:1483]
|
||||
- Fixed a bug in `<wa-spinner>` that caused `--track-width` to not work correctly [issue:1317]
|
||||
- Improved performance of `<wa-icon>` so initial rendering occurs faster, especially with multiple icons on the page [issue:1729]
|
||||
|
||||
## 3.0.0
|
||||
|
||||
@@ -21,6 +26,7 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
|
||||
- `<wa-badge>`
|
||||
- `<wa-button>`
|
||||
- `<wa-callout>`
|
||||
- `<wa-card>`
|
||||
- `<wa-details>`
|
||||
- `<wa-input>`
|
||||
- `<wa-select>`
|
||||
|
||||
@@ -22,7 +22,7 @@ As soon as all elements are registered _or_ after two seconds have elapsed, the
|
||||
|
||||
:::details Are you using Turbo in your app?
|
||||
|
||||
If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a change to register new components.
|
||||
If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a chance to register new components.
|
||||
|
||||
The following function acts as a middleware to ensure components are registered _before_ the page shows, eliminating FOUCE for page-to-page navigation with Turbo.
|
||||
|
||||
|
||||
@@ -16,27 +16,28 @@
|
||||
}
|
||||
|
||||
/* Appearance modifiers */
|
||||
:host([appearance~='plain']) {
|
||||
:host([appearance='plain']) {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host([appearance~='outlined']) {
|
||||
:host([appearance='outlined']) {
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border-color: var(--wa-color-surface-border);
|
||||
}
|
||||
|
||||
:host([appearance~='filled']) {
|
||||
:host([appearance='filled']) {
|
||||
background-color: var(--wa-color-neutral-fill-quiet);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
:host([appearance~='filled'][appearance~='outlined']) {
|
||||
border-color: var(--wa-color-neutral-border-quiet);
|
||||
:host([appearance='filled-outlined']) {
|
||||
background-color: var(--wa-color-neutral-fill-quiet);
|
||||
border-color: var(--wa-color-surface-border);
|
||||
}
|
||||
|
||||
:host([appearance~='accent']) {
|
||||
:host([appearance='accent']) {
|
||||
color: var(--wa-color-neutral-on-loud);
|
||||
background-color: var(--wa-color-neutral-fill-loud);
|
||||
border-color: transparent;
|
||||
|
||||
@@ -30,11 +30,19 @@ import styles from './card.css';
|
||||
export default class WaCard extends WebAwesomeElement {
|
||||
static css = [sizeStyles, styles];
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'media');
|
||||
private readonly hasSlotController = new HasSlotController(
|
||||
this,
|
||||
'footer',
|
||||
'header',
|
||||
'media',
|
||||
'header-actions',
|
||||
'footer-actions',
|
||||
'actions',
|
||||
);
|
||||
|
||||
/** The card's visual appearance. */
|
||||
@property({ reflect: true })
|
||||
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'outlined';
|
||||
appearance: 'accent' | 'filled' | 'outlined' | 'filled-outlined' | 'plain' = 'outlined';
|
||||
|
||||
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type WaDropdown from './dropdown.js';
|
||||
|
||||
describe('<wa-dropdown>', () => {
|
||||
it('should render a component', async () => {
|
||||
@@ -6,4 +8,112 @@ describe('<wa-dropdown>', () => {
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
|
||||
it('should respect the open attribute when included', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown open>
|
||||
<wa-button slot="trigger">Dropdown</wa-button>
|
||||
<wa-dropdown-item>One</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
|
||||
await el.updateComplete;
|
||||
await aTimeout(200);
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire a single show/after-show and hide/after-hide in normal open/close flow', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger">Dropdown</wa-button>
|
||||
<wa-dropdown-item>One</wa-dropdown-item>
|
||||
<wa-dropdown-item>Two</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
|
||||
// setup spies to track how often we see different show/hide events
|
||||
const showSpy = sinon.spy();
|
||||
const afterShowSpy = sinon.spy();
|
||||
const hideSpy = sinon.spy();
|
||||
const afterHideSpy = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-show', showSpy);
|
||||
el.addEventListener('wa-after-show', afterShowSpy);
|
||||
el.addEventListener('wa-hide', hideSpy);
|
||||
el.addEventListener('wa-after-hide', afterHideSpy);
|
||||
|
||||
// open the dropdown by triggering a click on the trigger
|
||||
const trigger = el.querySelector<HTMLElement>('[slot="trigger"]')!;
|
||||
trigger.click();
|
||||
|
||||
await waitUntil(() => showSpy.calledOnce);
|
||||
await waitUntil(() => afterShowSpy.calledOnce);
|
||||
|
||||
expect(showSpy.callCount).to.equal(1);
|
||||
expect(afterShowSpy.callCount).to.equal(1);
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
|
||||
// close the dropdown by clicking the trigger again
|
||||
trigger.click();
|
||||
|
||||
await waitUntil(() => hideSpy.calledOnce);
|
||||
await waitUntil(() => afterHideSpy.calledOnce);
|
||||
|
||||
expect(hideSpy.callCount).to.equal(1);
|
||||
expect(afterHideSpy.callCount).to.equal(1);
|
||||
|
||||
expect(el.open).to.be.false;
|
||||
});
|
||||
|
||||
it('should fire a single show/after-show and hide/after-hide when wa-hide event is cancelled', async () => {
|
||||
const el = await fixture<WaDropdown>(html`
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger">Dropdown</wa-button>
|
||||
<wa-dropdown-item>One</wa-dropdown-item>
|
||||
<wa-dropdown-item>Two</wa-dropdown-item>
|
||||
</wa-dropdown>
|
||||
`);
|
||||
|
||||
// setup spies to track how often we see different show/hide events
|
||||
const showSpy = sinon.spy();
|
||||
const afterShowSpy = sinon.spy();
|
||||
const hideSpy = sinon.spy();
|
||||
const afterHideSpy = sinon.spy();
|
||||
|
||||
el.addEventListener('wa-show', showSpy);
|
||||
el.addEventListener('wa-after-show', afterShowSpy);
|
||||
|
||||
// Intercept wa-hide and prevent it
|
||||
el.addEventListener('wa-hide', event => {
|
||||
event.preventDefault();
|
||||
hideSpy(event);
|
||||
});
|
||||
|
||||
el.addEventListener('wa-after-hide', afterHideSpy);
|
||||
|
||||
// open the dropdown by triggering a click on the trigger
|
||||
const trigger = el.querySelector<HTMLElement>('[slot="trigger"]')!;
|
||||
trigger.click();
|
||||
|
||||
await waitUntil(() => showSpy.calledOnce);
|
||||
await waitUntil(() => afterShowSpy.calledOnce);
|
||||
|
||||
expect(showSpy.callCount).to.equal(1);
|
||||
expect(afterShowSpy.callCount).to.equal(1);
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
|
||||
// click on the trigger (which should do nothing to the open state)
|
||||
trigger.click();
|
||||
|
||||
await waitUntil(() => hideSpy.calledOnce);
|
||||
|
||||
expect(hideSpy.callCount).to.equal(1);
|
||||
// after-hide should not have been called if hide is cancelled
|
||||
expect(afterHideSpy.callCount).to.equal(0);
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,6 +109,18 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
|
||||
async updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has('open')) {
|
||||
const previousOpen = changedProperties.get('open');
|
||||
// check if the previous value is the same
|
||||
// (if they are, do not trigger menu showing / hiding)
|
||||
if (previousOpen === this.open) {
|
||||
return;
|
||||
}
|
||||
// check if we are changing from undefined to false
|
||||
// (if we are, we can skip menu hiding)
|
||||
if (previousOpen === undefined && this.open === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.customStates.set('open', this.open);
|
||||
|
||||
if (this.open) {
|
||||
@@ -227,6 +239,12 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// if this dropdown is already open, do nothing
|
||||
// (this can happen when wa-hide was cancelled)
|
||||
if (this.popup.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
openDropdowns.forEach(dropdown => (dropdown.open = false));
|
||||
|
||||
this.popup.active = true; // Use wa-popup's active property instead of showPopover
|
||||
|
||||
@@ -187,7 +187,7 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch(['family', 'name', 'library', 'variant', 'src', 'autoWidth', 'swapOpacity'])
|
||||
@watch(['family', 'name', 'library', 'variant', 'src', 'autoWidth', 'swapOpacity'], { waitUntilFirstUpdate: true })
|
||||
async setIcon() {
|
||||
const { url, fromLibrary } = this.getIconSource();
|
||||
const library = fromLibrary ? getIconLibrary(this.library) : undefined;
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
padding: 0 var(--wa-form-control-padding-inline);
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
transition:
|
||||
background-color var(--wa-transition-normal),
|
||||
border var(--wa-transition-normal),
|
||||
|
||||
@@ -119,7 +119,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
@state() optionValues: Set<string | null> | undefined;
|
||||
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
@property({ reflect: true }) name = '';
|
||||
|
||||
private _defaultValue: null | string | string[] = null;
|
||||
|
||||
|
||||
@@ -2,58 +2,43 @@
|
||||
--track-width: 2px;
|
||||
--track-color: var(--wa-color-neutral-fill-normal);
|
||||
--indicator-color: var(--wa-color-brand-fill-loud);
|
||||
--speed: 2s;
|
||||
--speed: 1s;
|
||||
|
||||
/* Resizing a spinner element using anything but font-size will break the animation because the animation uses em units.
|
||||
Therefore, if a spinner is used in a flex container without `flex: none` applied, the spinner can grow/shrink and
|
||||
break the animation. The use of `flex: none` on the host element prevents this by always having the spinner sized
|
||||
according to its actual dimensions.
|
||||
*/
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
svg {
|
||||
[part='base'] {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
animation: spin var(--speed) linear infinite;
|
||||
}
|
||||
|
||||
.track {
|
||||
stroke: var(--track-color);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: var(--track-width) solid var(--track-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
stroke: var(--indicator-color);
|
||||
stroke-dasharray: 75, 100;
|
||||
stroke-dashoffset: -5;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
stroke-linecap: round;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: var(--track-width) solid transparent;
|
||||
border-top-color: var(--indicator-color);
|
||||
border-right-color: var(--indicator-color);
|
||||
border-radius: 50%;
|
||||
animation: spin var(--speed) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import styles from './spinner.css';
|
||||
* @since 2.0
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart track - The spinner's track.
|
||||
* @csspart indicator - The spinner's indicator.
|
||||
*
|
||||
* @cssproperty --track-width - The width of the track.
|
||||
* @cssproperty --track-color - The color of the track.
|
||||
@@ -25,17 +27,10 @@ export default class WaSpinner extends WebAwesomeElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<svg
|
||||
part="base"
|
||||
role="progressbar"
|
||||
aria-label=${this.localize.term('loading')}
|
||||
fill="none"
|
||||
viewBox="0 0 50 50"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle class="track" cx="25" cy="25" r="20" fill="none" stroke-width="5" />
|
||||
<circle class="indicator" cx="25" cy="25" r="20" fill="none" stroke-width="5" />
|
||||
</svg>
|
||||
<div part="base" role="progressbar" aria-label=${this.localize.term('loading')}>
|
||||
<div class="track" part="track"></div>
|
||||
<div class="indicator" part="indicator"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user