Api: Fix missing required validation & styling

This commit is contained in:
nathan 2024-03-15 14:13:56 -06:00
parent dbf77cb004
commit 50352cf36a
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} ?active=${this.open}
> >
<div <div
part="combobox" part="combobox base"
class="email__combobox" class="email__combobox"
slot="anchor" slot="anchor"
@keydown=${this.handleComboboxKeyDown} @keydown=${this.handleComboboxKeyDown}

View File

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

View File

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

View File

@ -118,11 +118,16 @@ export function inputBasicTests(before : Function, test_value : string, value_se
{ {
beforeEach(async() => beforeEach(async() =>
{ {
assert.isNotEmpty(test_value, "test_value needs to be a value");
element = await before(); 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 // 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 Not yet working attempt to have playwright compare visually
@ -142,5 +147,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. */ /** The select's help text. If you need to display HTML, use the `help-text` slot instead. */
@property({attribute: 'help-text'}) helpText = ''; @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 */ /** If the select is limited to 1 row, we show the number of tags not visible */
@state() @state()
protected _tagsHidden = 0; protected _tagsHidden = 0;
@ -817,9 +814,6 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
return this.select?.open ?? false; return this.select?.open ?? false;
} }
protected _renderOptions()
{return Promise.resolve();}
protected get select() : SlSelect protected get select() : SlSelect
{ {
return this.shadowRoot?.querySelector("sl-select"); return this.shadowRoot?.querySelector("sl-select");
@ -1034,7 +1028,7 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect)
return html` return html`
${this._styleTemplate()} ${this._styleTemplate()}
<sl-select <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} label=${this.label}
placeholder=${this.placeholder || (this.multiple && this.emptyLabel ? this.emptyLabel : "")} placeholder=${this.placeholder || (this.multiple && this.emptyLabel ? this.emptyLabel : "")}
?multiple=${this.multiple} ?multiple=${this.multiple}

View File

@ -8,7 +8,7 @@
*/ */
import {Et2InputWidget, Et2InputWidgetInterface} from "../Et2InputWidget/Et2InputWidget"; 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 {property} from "lit/decorators/property.js";
import {et2_readAttrWithDefault} from "../et2_core_xml"; import {et2_readAttrWithDefault} from "../et2_core_xml";
import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions"; import {cleanSelectOptions, find_select_options, SelectOption} from "./FindSelectOptions";
@ -140,26 +140,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() public getValueAsArray()
{ {
if(Array.isArray(this.value)) if(Array.isArray(this.value))
@ -200,48 +180,6 @@ export const Et2WidgetWithSelectMixin = <T extends Constructor<LitElement>>(supe
return result; return result;
} }
/**
* 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 * Set the select options
* *

View File

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

View File

@ -1,10 +1,17 @@
/** /**
* Test file for Etemplate webComponent Textbox * 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 {Et2Textbox} from "../Et2Textbox";
import {inputBasicTests} from "../../Et2InputWidget/test/InputBasicTests"; 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 // Reference to component under test
let element : Et2Textbox; let element : Et2Textbox;
@ -14,6 +21,11 @@ async function before()
element = await fixture<Et2Textbox>(html` element = await fixture<Et2Textbox>(html`
<et2-textbox></et2-textbox> <et2-textbox></et2-textbox>
`); `);
// Stub egw()
sinon.stub(element, "egw").returns(window.egw);
await elementUpdated(element);
return element; return element;
} }