diff --git a/src/widgets/forms/basic.ts b/src/widgets/forms/basic.ts index 0608d511..e9d5ff28 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,18 @@ export function basicField ( field.addEventListener( 'change', function (_e) { + 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 - 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 +266,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)) @@ -265,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 } 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 = {}