Support inheritance and initial values

This commit is contained in:
Lea Verou
2025-01-21 15:26:45 -05:00
parent e3560dcf98
commit 3dc526c948
4 changed files with 142 additions and 25 deletions

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

@@ -69,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' })

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

@@ -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
@@ -160,36 +168,69 @@ export default class WebAwesomeElement extends LitElement {
}
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;
},
});
}
}
}
}