From 06881912aa86fe4af26d962057ddfb489c69568e Mon Sep 17 00:00:00 2001 From: Robert Concepcion III Date: Tue, 25 May 2021 13:44:56 -0400 Subject: [PATCH 01/17] ids-input: add LABEL_HIDDEN + use unique ids and checks for label-el related content --- src/ids-base/ids-constants.js | 1 + src/ids-input/ids-input.js | 79 +++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/ids-base/ids-constants.js b/src/ids-base/ids-constants.js index 5651c8de78..6491f7a1ce 100644 --- a/src/ids-base/ids-constants.js +++ b/src/ids-base/ids-constants.js @@ -56,6 +56,7 @@ export const props = { KEEP_OPEN: 'keep-open', LABEL: 'label', LABEL_AUDIBLE: 'label-audible', + LABEL_HIDDEN: 'label-hidden', LABEL_REQUIRED: 'label-required', LABEL_FILETYPE: 'label-filetype', MAX: 'max', diff --git a/src/ids-input/ids-input.js b/src/ids-input/ids-input.js index c29ec9d083..51bc8cddf3 100644 --- a/src/ids-input/ids-input.js +++ b/src/ids-input/ids-input.js @@ -26,6 +26,8 @@ import { IdsTooltipMixin } from '../ids-mixins'; +let instanceCounter = 0; + // Properties observed by the Input const INPUT_PROPS = [ props.AUTOSELECT, @@ -37,7 +39,9 @@ const INPUT_PROPS = [ props.DISABLED, props.FIELD_HEIGHT, props.LABEL, + props.LABEL_HIDDEN, props.LABEL_REQUIRED, + props.ID, props.MODE, props.PLACEHOLDER, props.SIZE, @@ -151,8 +155,8 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { * @returns {string} The template */ template() { - if (!this.state || !this.state?.id) { - this.state = { id: 'ids-input-id' }; + if (!this.id) { + this.setAttribute?.(props.ID, `ids-input-${++instanceCounter}`); } // Input @@ -168,16 +172,25 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { let containerClass = `ids-input${inputState} ${this.size} ${this.fieldHeight}`; containerClass += stringUtils.stringToBool(this.compact) ? ' compact' : ''; - return ` -
- + const labelHtml = !this.label || this.getAttribute(props.LABEL_HIDDEN) ? '' : ( + `` + ); + + return ( + `
+ ${labelHtml}
- +
-
- `; +
` + ); } /** @@ -185,7 +198,7 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { * @returns {HTMLInputElement} the inner `input` element */ get input() { - return this.shadowRoot?.querySelector(`#${this.state.id}`); + return this.shadowRoot?.querySelector(`#${this.id}-input`); } /** @@ -193,7 +206,7 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { * @returns {HTMLLabelElement} the inner `label` element */ get labelEl() { - return this.shadowRoot?.querySelector(`[for="${this.state.id}"]`); + return this.shadowRoot?.querySelector(`label`); } /** @@ -210,20 +223,21 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { prop2: prop !== props.READONLY ? props.READONLY : props.DISABLED, val: stringUtils.stringToBool(this[prop]) }; + if (options.val) { this.input?.removeAttribute(options.prop2); - this.container.classList.remove(options.prop2); - this.container.querySelector('ids-text').removeAttribute(options.prop2); + this.container?.classList?.remove?.(options.prop2); + this.container?.querySelector?.('ids-text')?.removeAttribute(options.prop2); msgNodes.forEach((x) => x.classList.remove(options.prop2)); this.input?.setAttribute(options.prop1, 'true'); this.container.classList.add(options.prop1); - this.container.querySelector('ids-text').setAttribute(options.prop1, 'true'); + this.container?.querySelector?.('ids-text')?.setAttribute?.(options.prop1, 'true'); msgNodes.forEach((x) => x.classList.add(options.prop1)); } else { this.input?.removeAttribute(options.prop1); this.container.classList.remove(options.prop1); - this.container.querySelector('ids-text').removeAttribute(options.prop1); + this.container.querySelector('ids-text')?.removeAttribute(options.prop1); msgNodes.forEach((x) => x.classList.remove(options.prop1)); } } @@ -236,12 +250,27 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { * @returns {void} */ setLabelText(value) { - const labelText = this.shadowRoot.querySelector(`[for="${this.state.id}"] ids-text`); + const labelText = this.shadowRoot.querySelector(`[for="${this.id}-input"] ids-text`); if (labelText) { labelText.innerHTML = value || ''; } } + /** + * Sets a label's text as not displayed in explicit label element + */ + set labelHidden(value) { + if (stringUtils.stringToBool(value)) { + this?.setAttribute(props.LABEL_HIDDEN, ''); + } else { + this?.removeAttribute(props.LABEL_HIDDEN); + } + } + + get labelHidden() { + return this.getAttribute(props.LABEL_HIDDEN); + } + /** * Get field height css class name with prefix * @private @@ -444,6 +473,7 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { */ set dirtyTracker(value) { const val = stringUtils.stringToBool(value); + if (val) { this.setAttribute(props.DIRTY_TRACKER, val.toString()); } else { @@ -664,6 +694,21 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { get value() { return this.input?.value || ''; } + + /** + * set the id of the input, which will also determine the + * input id for labels at #${id}-input + * + * @param {string} value id + */ + set id(value) { + this.setAttribute(props.ID, value); + this.input?.setAttribute(props.ID, `${value}-input`); + } + + get id() { + return this.getAttribute(props.ID); + } } export default IdsInput; From fbf1ee5bd363d2d1e8a3f613cd09cef051512a40 Mon Sep 17 00:00:00 2001 From: Robert Concepcion III Date: Tue, 25 May 2021 13:47:44 -0400 Subject: [PATCH 02/17] ids-input: update all tests to run with new changes (still need 1 for dirty tracking) --- test/ids-input/ids-input-func-test.js | 33 +++++++++++-------- test/ids-input/ids-input-func-test.js.snap | 2 +- .../ids-input-validation-func-test.js | 20 +++++++---- .../ids-trigger-field-func-test.js.snap | 4 +-- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/test/ids-input/ids-input-func-test.js b/test/ids-input/ids-input-func-test.js index a0dcf591ad..ff4fd24639 100644 --- a/test/ids-input/ids-input-func-test.js +++ b/test/ids-input/ids-input-func-test.js @@ -4,13 +4,21 @@ import IdsInput from '../../src/ids-input/ids-input'; import IdsClearableMixin from '../../src/ids-mixins/ids-clearable-mixin'; +const processAnimFrame = () => new Promise((resolve) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(resolve); + }); +}); + describe('IdsInput Component', () => { let input; beforeEach(async () => { - const elem = new IdsInput(); - document.body.appendChild(elem); - input = document.querySelector('ids-input'); + const template = document.createElement('template'); + template.innerHTML = ''; + input = template.content.childNodes[0]; + document.body.appendChild(input); + await processAnimFrame(); }); afterEach(async () => { @@ -19,9 +27,6 @@ describe('IdsInput Component', () => { it('renders with no errors', () => { const errors = jest.spyOn(global.console, 'error'); - const elem = new IdsInput(); - document.body.appendChild(elem); - elem.remove(); expect(document.querySelectorAll('ids-input').length).toEqual(1); expect(errors).not.toHaveBeenCalled(); }); @@ -99,18 +104,20 @@ describe('IdsInput Component', () => { }); it('should set label text', () => { - let label = input.labelEl.querySelector('ids-text'); - label?.remove(); input.label = 'test'; document.body.innerHTML = ''; - const elem = new IdsInput(); + + const template = document.createElement('template'); + template.innerHTML = ''; + const elem = template.content.childNodes[0]; + document.body.appendChild(elem); + document.body.appendChild(elem); input = document.querySelector('ids-input'); - label = input.labelEl.querySelector('ids-text'); - expect(input.labelEl.textContent.trim()).toBe(''); - input.label = 'test'; - expect(input.labelEl.textContent.trim()).toBe('test'); + expect(input.labelEl.textContent.trim()).toBe('Hello World'); + input.label = 'test2'; + expect(input.labelEl.textContent.trim()).toBe('test2'); input.label = null; expect(input.labelEl.textContent.trim()).toBe(''); }); diff --git a/test/ids-input/ids-input-func-test.js.snap b/test/ids-input/ids-input-func-test.js.snap index 315f38c09d..b72008afb6 100644 --- a/test/ids-input/ids-input-func-test.js.snap +++ b/test/ids-input/ids-input-func-test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`IdsInput Component renders correctly 1`] = `""`; +exports[`IdsInput Component renders correctly 1`] = `""`; diff --git a/test/ids-input/ids-input-validation-func-test.js b/test/ids-input/ids-input-validation-func-test.js index 5515be8a27..2e3a3973ab 100644 --- a/test/ids-input/ids-input-validation-func-test.js +++ b/test/ids-input/ids-input-validation-func-test.js @@ -5,10 +5,19 @@ import IdsInput from '../../src/ids-input/ids-input'; let elem = null; +const processAnimFrame = () => new Promise((resolve) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(resolve); + }); +}); + describe('IdsInput Component', () => { beforeEach(async () => { - elem = new IdsInput(); + const template = document.createElement('template'); + template.innerHTML = ''; + elem = template.content.childNodes[0]; document.body.appendChild(elem); + await processAnimFrame(); }); afterEach(async () => { @@ -16,10 +25,10 @@ describe('IdsInput Component', () => { elem = null; }); - it('should add/remove required error', () => { + it('should add/remove required error', async () => { elem.validate = 'required'; elem.template(); - document.body.appendChild(elem); + await processAnimFrame(); expect(elem.getAttribute('validate')).toEqual('required'); expect(elem.validate).toEqual('required'); @@ -29,10 +38,10 @@ describe('IdsInput Component', () => { const msgEl = elem.shadowRoot.querySelector('.validation-message'); expect(elem.input.getAttribute('aria-invalid')).toEqual('true'); - expect(elem.input.getAttribute('aria-describedby')).toEqual('ids-input-id-error'); + expect(elem.input.getAttribute('aria-describedby')).toEqual('ids-input-1-input-error'); expect(msgEl).toBeTruthy(); expect(msgEl.getAttribute('validation-id')).toEqual('required'); - expect(msgEl.getAttribute('id')).toEqual('ids-input-id-error'); + expect(msgEl.getAttribute('id')).toEqual('ids-input-1-input-error'); elem.input.value = 'test'; elem.checkValidation(); @@ -224,7 +233,6 @@ describe('IdsInput Component', () => { it('should remove all the messages from input', () => { elem.validate = 'required'; elem.template(); - document.body.appendChild(elem); expect(elem.getAttribute('validate')).toEqual('required'); expect(elem.validate).toEqual('required'); diff --git a/test/ids-trigger-field/ids-trigger-field-func-test.js.snap b/test/ids-trigger-field/ids-trigger-field-func-test.js.snap index 65a758236e..263ae205fa 100644 --- a/test/ids-trigger-field/ids-trigger-field-func-test.js.snap +++ b/test/ids-trigger-field/ids-trigger-field-func-test.js.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`IdsTriggerField Component renders correctly 1`] = `""`; +exports[`IdsTriggerField Component renders correctly 1`] = `""`; -exports[`IdsTriggerField Component renders correctly 2`] = `""`; +exports[`IdsTriggerField Component renders correctly 2`] = `""`; From 2440e299f2d7cf4ece18ce979c4c138e6a1040b0 Mon Sep 17 00:00:00 2001 From: Robert Concepcion III Date: Tue, 25 May 2021 14:12:55 -0400 Subject: [PATCH 03/17] ids-input: fix dirty test after updating --- test/ids-input/ids-input-func-test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/ids-input/ids-input-func-test.js b/test/ids-input/ids-input-func-test.js index ff4fd24639..6a9d759a90 100644 --- a/test/ids-input/ids-input-func-test.js +++ b/test/ids-input/ids-input-func-test.js @@ -243,7 +243,7 @@ describe('IdsInput Component', () => { expect(input.input.classList).not.toContain('text-ellipsis'); }); - it('should setup dirty tracking', () => { + it('should setup dirty tracking', async () => { input.dirtyTracker = true; input.input.remove(); input.dirtyTrackerEvents(); @@ -257,6 +257,12 @@ describe('IdsInput Component', () => { input.handleDirtyTracker(); expect(input.dirty).toEqual({ original: '' }); document.body.innerHTML = ''; + const template = document.createElement('template'); + template.innerHTML = ''; + elem = template.content.childNodes[0]; + document.body.appendChild(elem); + await processAnimFrame(); + elem = new IdsInput(); document.body.appendChild(elem); input = document.querySelector('ids-input'); From 91753e444f96469ec0ad4e0fa8e369168212def2 Mon Sep 17 00:00:00 2001 From: Robert Concepcion III Date: Wed, 26 May 2021 17:28:15 -0400 Subject: [PATCH 04/17] ids-input: changes from ids-spinbox branch to allow external label styling and input-required functionality --- app/ids-input/standalone-css.html | 36 +++++++------- src/ids-input/README.md | 1 + src/ids-input/ids-input.js | 37 ++++++++++++--- src/ids-input/ids-input.scss | 79 ++++++++++++++++--------------- 4 files changed, 90 insertions(+), 63 deletions(-) diff --git a/app/ids-input/standalone-css.html b/app/ids-input/standalone-css.html index 23e4c413d4..9923dd56a3 100644 --- a/app/ids-input/standalone-css.html +++ b/app/ids-input/standalone-css.html @@ -14,49 +14,49 @@

IDS Input - Standalone CSS

- +
- +
- +
- +
- +
- +
- +
@@ -73,7 +73,7 @@

Ids Input - Compact

- +
@@ -91,28 +91,28 @@

Ids Input - Field Heights (Height)

- +
- +
- +
- +
@@ -130,42 +130,42 @@

Ids Input - Sizes (Width)

- +
- +
- +
- +
- +
- +
diff --git a/src/ids-input/README.md b/src/ids-input/README.md index fe48ac15b6..479ba2783e 100644 --- a/src/ids-input/README.md +++ b/src/ids-input/README.md @@ -142,5 +142,6 @@ The IDS Input component is now a WebComponent. Instead of using classes to defin 1. Can be consumed in NG/Vue/React (pull it in standalone/built see it works standalone) ## Accessibility Guidelines +There should be a label on all inputs to give an indication what the value means. ## Regional Considerations diff --git a/src/ids-input/ids-input.js b/src/ids-input/ids-input.js index 51bc8cddf3..0e84bca619 100644 --- a/src/ids-input/ids-input.js +++ b/src/ids-input/ids-input.js @@ -173,7 +173,7 @@ class IdsInput extends mix(IdsElement).with(...appliedMixins) { containerClass += stringUtils.stringToBool(this.compact) ? ' compact' : ''; const labelHtml = !this.label || this.getAttribute(props.LABEL_HIDDEN) ? '' : ( - `