From c3ecc732c4a395bb285db33cacffac1e45bfb037 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 15 Mar 2026 18:31:02 +1100 Subject: [PATCH 1/5] fix year entry for dates in forms --- src/widgets/forms/basic.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/widgets/forms/basic.ts b/src/widgets/forms/basic.ts index 0608d511..3e725653 100644 --- a/src/widgets/forms/basic.ts +++ b/src/widgets/forms/basic.ts @@ -136,6 +136,8 @@ export function basicField ( ;(field as any).style = inputStyle rhs.appendChild(field) field.setAttribute('type', params.type ? params.type : 'text') + const fieldType = (field.getAttribute('type') || '').toLowerCase() + const deferWhileFocused = fieldType === 'date' || fieldType === 'datetime-local' const size = kb.anyJS(form, ns.ui('size')) || styleConstants.textInputSize || 20 field.setAttribute('size', size) @@ -189,9 +191,13 @@ export function basicField ( field.addEventListener( 'change', function (_e) { + if (deferWhileFocused && dom.activeElement === field) return // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return - field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. + const disabledForSave = !deferWhileFocused + if (disabledForSave) { + field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. + } field.setAttribute('style', inputStyle + 'color: gray;') // pending const ds = kb.statementsMatching(subject, property as any) // remove any multiple values let result @@ -255,7 +261,9 @@ export function basicField ( updateMany(ds, is as any, function (uri, ok, body) { // kb.updater.update(ds, is, function (uri, ok, body) { if (ok) { - field.disabled = false + if (disabledForSave) { + field.disabled = false + } field.setAttribute('style', inputStyle) } else { box.appendChild(errorMessageBlock(dom, body)) From 53d9f22abdf00887fae64f5660ecaf9221e7e0c4 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 15 Mar 2026 18:56:54 +1100 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/widgets/forms/basic.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/widgets/forms/basic.ts b/src/widgets/forms/basic.ts index 3e725653..1b718d13 100644 --- a/src/widgets/forms/basic.ts +++ b/src/widgets/forms/basic.ts @@ -194,10 +194,9 @@ export function basicField ( if (deferWhileFocused && dom.activeElement === field) return // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return - const disabledForSave = !deferWhileFocused - if (disabledForSave) { - field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. - } + // Always disable during async save to prevent duplicate/overlapping updates, + // including for date/datetime-local fields. + field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. field.setAttribute('style', inputStyle + 'color: gray;') // pending const ds = kb.statementsMatching(subject, property as any) // remove any multiple values let result From def750d743e02e0acfed8e65bbcf95e79dfd93eb Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 15 Mar 2026 19:05:23 +1100 Subject: [PATCH 3/5] modified tests --- test/unit/widgets/forms/basic.test.ts | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/test/unit/widgets/forms/basic.test.ts b/test/unit/widgets/forms/basic.test.ts index 082625a9..5547d2e3 100644 --- a/test/unit/widgets/forms/basic.test.ts +++ b/test/unit/widgets/forms/basic.test.ts @@ -295,6 +295,126 @@ describe('basicField', () => { expect(store.updater.updated).toEqual(true) }) + it('defers date change while focused', () => { + const container = document.createElement('div') + document.body.appendChild(container) + const already = {} + const subject = namedNode('http://example.com/#this') + const form = namedNode('http://example.com/#form') + const formType = ns.ui('DateField') + const property = namedNode('http://example.com/#some-property') + const doc = namedNode('http://example.com/') + const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263 + store.add(form, ns.ui('property'), property, doc) + store.add(form, ns.rdf('type'), formType, doc) + store.add(subject, property, '2026-03-15', doc) + + const originalUpdate = store.updater.update + const updateSpy = jest.fn((_deletes, _inserts, onDone) => { + onDone('uri', true, 'body') + return Promise.resolve() + }) + store.updater.update = updateSpy as any + + try { + const result = basicField( + document, + container, + already, + subject, + form, + doc, + callbackFunction + ) + const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement + inputElement.focus() + inputElement.value = '2026-03-16' + inputElement.dispatchEvent(new Event('change')) + expect(updateSpy).not.toHaveBeenCalled() + } finally { + store.updater.update = originalUpdate + container.remove() + } + }) + + it('does not disable DateField during save', () => { + const container = document.createElement('div') + const already = {} + const subject = namedNode('http://example.com/#this') + const form = namedNode('http://example.com/#form') + const formType = ns.ui('DateField') + const property = namedNode('http://example.com/#some-property') + const doc = namedNode('http://example.com/') + const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263 + store.add(form, ns.ui('property'), property, doc) + store.add(form, ns.rdf('type'), formType, doc) + store.add(subject, property, '2026-03-15', doc) + + const result = basicField( + document, + container, + already, + subject, + form, + doc, + callbackFunction + ) + const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement + + const originalUpdate = store.updater.update + store.updater.update = ((_deletes, _inserts, onDone) => { + expect(inputElement.disabled).toEqual(false) + onDone('uri', true, 'body') + return Promise.resolve() + }) as any + + try { + inputElement.value = '2026-03-16' + inputElement.dispatchEvent(new Event('change')) + expect(inputElement.disabled).toEqual(false) + } finally { + store.updater.update = originalUpdate + } + }) + + it('disables non-date input during save and reenables on success', () => { + const container = document.createElement('div') + const already = {} + const subject = namedNode('http://example.com/#this') + const form = namedNode('http://example.com/#form') + const property = namedNode('http://example.com/#some-property') + const doc = namedNode('http://example.com/') + const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263 + store.add(form, ns.ui('property'), property, doc) + store.add(subject, property, namedNode('http://example.com/#initial-value'), doc) + + const result = basicField( + document, + container, + already, + subject, + form, + doc, + callbackFunction + ) + const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement + + const originalUpdate = store.updater.update + store.updater.update = ((_deletes, _inserts, onDone) => { + expect(inputElement.disabled).toEqual(true) + onDone('uri', true, 'body') + return Promise.resolve() + }) as any + + try { + inputElement.value = 'changed value' + inputElement.dispatchEvent(new Event('change')) + expect(inputElement.disabled).toEqual(false) + } finally { + store.updater.update = originalUpdate + } + }) + it('calls updater on change for a NamedNodeUriField', () => { const container = document.createElement('div') const already = {} From eea553df42e49df0acda87cff4bc1d8fdb11a033 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 15 Mar 2026 19:10:27 +1100 Subject: [PATCH 4/5] do not affect 2 date fix --- src/widgets/forms/basic.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/widgets/forms/basic.ts b/src/widgets/forms/basic.ts index 1b718d13..3e725653 100644 --- a/src/widgets/forms/basic.ts +++ b/src/widgets/forms/basic.ts @@ -194,9 +194,10 @@ export function basicField ( if (deferWhileFocused && dom.activeElement === field) return // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return - // Always disable during async save to prevent duplicate/overlapping updates, - // including for date/datetime-local fields. - field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. + const disabledForSave = !deferWhileFocused + if (disabledForSave) { + field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker. + } field.setAttribute('style', inputStyle + 'color: gray;') // pending const ds = kb.statementsMatching(subject, property as any) // remove any multiple values let result From 6b094f51dd1011e2bdb80cce81f2589ce9d95290 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sun, 15 Mar 2026 19:13:02 +1100 Subject: [PATCH 5/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/widgets/forms/basic.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/widgets/forms/basic.ts b/src/widgets/forms/basic.ts index 3e725653..e9d5ff28 100644 --- a/src/widgets/forms/basic.ts +++ b/src/widgets/forms/basic.ts @@ -191,7 +191,12 @@ export function basicField ( field.addEventListener( 'change', function (_e) { - if (deferWhileFocused && dom.activeElement === field) return + if (deferWhileFocused && dom.activeElement === field) { + if (field.dataset) { + field.dataset.deferredChange = 'true' + } + return + } // i.e. lose focus with changed data if (params.pattern && !field.value.match(params.pattern)) return const disabledForSave = !deferWhileFocused @@ -273,5 +278,20 @@ export function basicField ( }, true ) + field.addEventListener( + 'blur', + function (_e) { + if ( + deferWhileFocused && + field.dataset && + field.dataset.deferredChange === 'true' + ) { + delete field.dataset.deferredChange + const event = new Event('change', { bubbles: true }) + field.dispatchEvent(event) + } + }, + true + ) return box }