Api: Fix missing required validation & styling

This commit is contained in:
nathan 2024-03-15 14:13:56 -06:00 committed by ralf
parent f314efabf2
commit 8f2ebf9bd6
8 changed files with 57 additions and 77 deletions

View File

@ -1351,7 +1351,7 @@ export class Et2Email extends Et2InputWidget(LitElement) implements SearchMixinI
?active=${this.open}
>
<div
part="combobox"
part="combobox base"
class="email__combobox"
slot="anchor"
@keydown=${this.handleComboboxKeyDown}

View File

@ -207,4 +207,4 @@ inputBasicTests(async() =>
const element = await before();
element.noLang = true;
return element
}, "", "input");
}, "fake@example.com", "input");

View File

@ -188,6 +188,7 @@ const Et2InputWidgetMixin = <T extends Constructor<LitElement>>(superclass : T)
this._messagesHeldWhileFocused = [];
this.readonly = false;
this.required = false;
this._oldValue = this.getValue();
this.isSlComponent = typeof (<any>this).handleChange === 'function';
@ -223,7 +224,7 @@ const Et2InputWidgetMixin = <T extends Constructor<LitElement>>(superclass : T)
* A property has changed, and we want to make adjustments to other things
* based on that
*
* @param {import('@lion/core').PropertyValues } changedProperties
* @param changedProperties
*/
updated(changedProperties : PropertyValues)
{

View File

@ -117,11 +117,16 @@ export function inputBasicTests(before : Function, test_value : string, value_se
{
beforeEach(async() =>
{
assert.isNotEmpty(test_value, "test_value needs to be a value");
element = await before();
await elementUpdated(<Element><unknown>element);
element.required = true;
await elementUpdated(<Element><unknown>element);
});
// This is just visually comparing for a difference, no deep inspection
it("looks different when required")
//it("looks different when required")
/*
Not yet working attempt to have playwright compare visually
@ -141,5 +146,35 @@ export function inputBasicTests(before : Function, test_value : string, value_se
*/
it("is invalid without a value", async() =>
{
element.set_value("");
// wait for asychronous changes to the DOM
await elementUpdated(<Element><unknown>element);
// widget returns what we gave it
assert.equal(element.get_value(), "");
assert.equal(element.required, true, "required not set");
// widget fails validation
let messages = [];
assert.isFalse(element.isValid(messages), `Required has no value (${element.getValue()}), but is considered valid`);
});
it("is valid with a value", async() =>
{
element.set_value(test_value);
// wait for asychronous changes to the DOM
await elementUpdated(<Element><unknown>element);
// widget returns what we gave it
assert.equal(element.get_value(), test_value);
assert.equal(element.required, true, "required not set");
// widget fails validation
let messages = [];
assert.isTrue(element.isValid(messages), `Required has a value (${element.getValue()}), but is not considered valid. ` + messages.join("\n"));
});
});
}

View File

@ -293,9 +293,6 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
/** The select's help text. If you need to display HTML, use the `help-text` slot instead. */
@property({attribute: 'help-text'}) helpText = '';
/** The select's required attribute. */
@property({type: Boolean, reflect: true}) required = false;
/** If the select is limited to 1 row, we show the number of tags not visible */
@state()
protected _tagsHidden = 0;
@ -817,9 +814,6 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
return this.select?.open ?? false;
}
protected _renderOptions()
{return Promise.resolve();}
protected get select() : SlSelect
{
return this.shadowRoot?.querySelector("sl-select");
@ -1034,7 +1028,7 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
return html`
${this._styleTemplate()}
<sl-select
exportparts="prefix, tags, display-input, expand-icon, combobox, listbox, option"
exportparts="prefix, tags, display-input, expand-icon, combobox, combobox:base, listbox, option"
label=${this.label}
placeholder=${this.placeholder || (this.multiple && this.emptyLabel ? this.emptyLabel : "")}
?multiple=${this.multiple}

View File

@ -8,7 +8,7 @@
*/
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget";
import {html, LitElement, PropertyValues, render, TemplateResult} from "lit";
import {html, LitElement, PropertyValues, TemplateResult} from "lit";
import {property} from "lit/decorators/property.js";
import {et2_readAttrWithDefault} from "../et2_core_xml";
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
@ -134,26 +134,6 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
}
willUpdate(changedProperties : PropertyValues<this>)
{
// Add in actual option tags to the DOM based on the new select_options
if(changedProperties.has('select_options') || changedProperties.has("emptyLabel"))
{
// Add in options as children to the target node
const optionPromise = this._renderOptions();
// This is needed to display initial load value in some cases, like infolog nm header filters
if(typeof this.selectionChanged !== "undefined")
{
optionPromise.then(async() =>
{
await this.updateComplete;
this.selectionChanged();
});
}
}
}
public getValueAsArray()
{
if(Array.isArray(this.value))
@ -188,48 +168,6 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
return search(options ?? this.select_options, value);
}
/**
* Render select_options as child DOM Nodes
* @protected
*/
protected _renderOptions()
{
return Promise.resolve();
// Add in options as children to the target node
if(!this._optionTargetNode)
{
return Promise.resolve();
}
/**
* Doing all this garbage to get the options to always show up.
* If we just do `render(options, target)`, they only show up in the DOM the first time. If the
* same option comes back in a subsequent search, map() does not put it into the DOM.
* If we render into a new target, the options get rendered, but we have to wait for them to be
* rendered before we can do anything else with them.
*/
let temp_target = document.createElement("div");
let options = html`${this._emptyLabelTemplate()}${this.select_options
// Filter out empty values if we have empty label to avoid duplicates
.filter(o => this.emptyLabel ? o.value !== '' : o)
.map(this._groupTemplate.bind(this))}`;
render(options, temp_target);
this._optionRenderPromise = Promise.all(([...temp_target.querySelectorAll(":scope > *")].map(item => item.render)))
.then(() =>
{
this._optionTargetNode.replaceChildren(
...Array.from(temp_target.querySelectorAll(":scope > *")),
...Array.from(this._optionTargetNode.querySelectorAll(":scope > [slot]"))
);
if(typeof this.handleMenuSlotChange == "function")
{
this.handleMenuSlotChange();
}
});
return this._optionRenderPromise;
}
/**
* Set the select options
*

View File

@ -157,6 +157,6 @@ inputBasicTests(async() =>
{
const element = await before();
element.noLang = true;
element.select_options = [{value: "", label: ""}];
element.select_options = [{value: "", label: ""}, {value: "one", label: "one"}];
return element
}, "", "sl-select");
}, "one", "sl-select");

View File

@ -1,10 +1,17 @@
/**
* Test file for Etemplate webComponent Textbox
*/
import {assert, fixture, html} from '@open-wc/testing';
import {assert, elementUpdated, fixture, html} from '@open-wc/testing';
import {Et2Textbox} from "../Et2Textbox";
import {inputBasicTests} from "../../Et2InputWidget/test/InputBasicTests";
import * as sinon from "sinon";
// Stub global egw for cssImage to find
// @ts-ignore
window.egw = {
lang: i => i + "*",
tooltipUnbind: () => {}
};
// Reference to component under test
let element : Et2Textbox;
@ -14,6 +21,11 @@ async function before()
element = await fixture<Et2Textbox>(html`
<et2-textbox></et2-textbox>
`);
// Stub egw()
sinon.stub(element, "egw").returns(window.egw);
await elementUpdated(element);
return element;
}