Skip to content

Commit 91a1254

Browse files
authored
Merge branch 'main' into profileEditA11y
2 parents 81e7823 + f2c288b commit 91a1254

File tree

3 files changed

+159
-3
lines changed

3 files changed

+159
-3
lines changed

src/acl/access-groups.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,15 @@ export class AccessGroups {
258258
}
259259
return this.handleDroppedUri(uri, combo, true)
260260
} else if (!agent) {
261-
const error = ` Error: Drop fails to drop appropriate thing! ${uri}`
261+
const detectedTypes = Object.keys(this.store.findTypeURIs(thing))
262+
const typeDetails = detectedTypes.length > 0
263+
? `Detected RDF types: ${detectedTypes.join(', ')}`
264+
: 'No RDF type was detected for this URI.'
265+
const error =
266+
`Error: Failed to add access target: ${uri} is not a recognized ACL target type.` +
267+
` Expected one of: vcard:WebID, vcard:Group, foaf:Person, foaf:Agent, solid:AppProvider, solid:AppProviderClass, or recognized ACL classes.` +
268+
' Hint: try dropping a WebID profile URI, a vcard:Group URI, or a web app origin.' +
269+
typeDetails
262270
debug.error(error)
263271
return Promise.reject(new Error(error))
264272
}

src/widgets/forms/basic.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export function basicField (
136136
;(field as any).style = inputStyle
137137
rhs.appendChild(field)
138138
field.setAttribute('type', params.type ? params.type : 'text')
139+
const fieldType = (field.getAttribute('type') || '').toLowerCase()
140+
const deferWhileFocused = fieldType === 'date' || fieldType === 'datetime-local'
139141

140142
const size = kb.anyJS(form, ns.ui('size')) || styleConstants.textInputSize || 20
141143
field.setAttribute('size', size)
@@ -189,9 +191,18 @@ export function basicField (
189191
field.addEventListener(
190192
'change',
191193
function (_e) {
194+
if (deferWhileFocused && dom.activeElement === field) {
195+
if (field.dataset) {
196+
field.dataset.deferredChange = 'true'
197+
}
198+
return
199+
}
192200
// i.e. lose focus with changed data
193201
if (params.pattern && !field.value.match(params.pattern)) return
194-
field.disabled = true // See if this stops getting two dates from fumbling e.g the chrome datepicker.
202+
const disabledForSave = !deferWhileFocused
203+
if (disabledForSave) {
204+
field.disabled = true // See if this stops getting two dates from fumbling, e.g., the chrome datepicker.
205+
}
195206
field.setAttribute('style', inputStyle + 'color: gray;') // pending
196207
const ds = kb.statementsMatching(subject, property as any) // remove any multiple values
197208
let result
@@ -255,7 +266,9 @@ export function basicField (
255266
updateMany(ds, is as any, function (uri, ok, body) {
256267
// kb.updater.update(ds, is, function (uri, ok, body) {
257268
if (ok) {
258-
field.disabled = false
269+
if (disabledForSave) {
270+
field.disabled = false
271+
}
259272
field.setAttribute('style', inputStyle)
260273
} else {
261274
box.appendChild(errorMessageBlock(dom, body))
@@ -265,5 +278,20 @@ export function basicField (
265278
},
266279
true
267280
)
281+
field.addEventListener(
282+
'blur',
283+
function (_e) {
284+
if (
285+
deferWhileFocused &&
286+
field.dataset &&
287+
field.dataset.deferredChange === 'true'
288+
) {
289+
delete field.dataset.deferredChange
290+
const event = new Event('change', { bubbles: true })
291+
field.dispatchEvent(event)
292+
}
293+
},
294+
true
295+
)
268296
return box
269297
}

test/unit/widgets/forms/basic.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,126 @@ describe('basicField', () => {
295295
expect(store.updater.updated).toEqual(true)
296296
})
297297

298+
it('defers date change while focused', () => {
299+
const container = document.createElement('div')
300+
document.body.appendChild(container)
301+
const already = {}
302+
const subject = namedNode('http://example.com/#this')
303+
const form = namedNode('http://example.com/#form')
304+
const formType = ns.ui('DateField')
305+
const property = namedNode('http://example.com/#some-property')
306+
const doc = namedNode('http://example.com/')
307+
const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263
308+
store.add(form, ns.ui('property'), property, doc)
309+
store.add(form, ns.rdf('type'), formType, doc)
310+
store.add(subject, property, '2026-03-15', doc)
311+
312+
const originalUpdate = store.updater.update
313+
const updateSpy = jest.fn((_deletes, _inserts, onDone) => {
314+
onDone('uri', true, 'body')
315+
return Promise.resolve()
316+
})
317+
store.updater.update = updateSpy as any
318+
319+
try {
320+
const result = basicField(
321+
document,
322+
container,
323+
already,
324+
subject,
325+
form,
326+
doc,
327+
callbackFunction
328+
)
329+
const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement
330+
inputElement.focus()
331+
inputElement.value = '2026-03-16'
332+
inputElement.dispatchEvent(new Event('change'))
333+
expect(updateSpy).not.toHaveBeenCalled()
334+
} finally {
335+
store.updater.update = originalUpdate
336+
container.remove()
337+
}
338+
})
339+
340+
it('does not disable DateField during save', () => {
341+
const container = document.createElement('div')
342+
const already = {}
343+
const subject = namedNode('http://example.com/#this')
344+
const form = namedNode('http://example.com/#form')
345+
const formType = ns.ui('DateField')
346+
const property = namedNode('http://example.com/#some-property')
347+
const doc = namedNode('http://example.com/')
348+
const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263
349+
store.add(form, ns.ui('property'), property, doc)
350+
store.add(form, ns.rdf('type'), formType, doc)
351+
store.add(subject, property, '2026-03-15', doc)
352+
353+
const result = basicField(
354+
document,
355+
container,
356+
already,
357+
subject,
358+
form,
359+
doc,
360+
callbackFunction
361+
)
362+
const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement
363+
364+
const originalUpdate = store.updater.update
365+
store.updater.update = ((_deletes, _inserts, onDone) => {
366+
expect(inputElement.disabled).toEqual(false)
367+
onDone('uri', true, 'body')
368+
return Promise.resolve()
369+
}) as any
370+
371+
try {
372+
inputElement.value = '2026-03-16'
373+
inputElement.dispatchEvent(new Event('change'))
374+
expect(inputElement.disabled).toEqual(false)
375+
} finally {
376+
store.updater.update = originalUpdate
377+
}
378+
})
379+
380+
it('disables non-date input during save and reenables on success', () => {
381+
const container = document.createElement('div')
382+
const already = {}
383+
const subject = namedNode('http://example.com/#this')
384+
const form = namedNode('http://example.com/#form')
385+
const property = namedNode('http://example.com/#some-property')
386+
const doc = namedNode('http://example.com/')
387+
const callbackFunction = jest.fn() // TODO: https://github.com/solidos/solid-ui/issues/263
388+
store.add(form, ns.ui('property'), property, doc)
389+
store.add(subject, property, namedNode('http://example.com/#initial-value'), doc)
390+
391+
const result = basicField(
392+
document,
393+
container,
394+
already,
395+
subject,
396+
form,
397+
doc,
398+
callbackFunction
399+
)
400+
const inputElement = result.childNodes[1].childNodes[0] as HTMLInputElement
401+
402+
const originalUpdate = store.updater.update
403+
store.updater.update = ((_deletes, _inserts, onDone) => {
404+
expect(inputElement.disabled).toEqual(true)
405+
onDone('uri', true, 'body')
406+
return Promise.resolve()
407+
}) as any
408+
409+
try {
410+
inputElement.value = 'changed value'
411+
inputElement.dispatchEvent(new Event('change'))
412+
expect(inputElement.disabled).toEqual(false)
413+
} finally {
414+
store.updater.update = originalUpdate
415+
}
416+
})
417+
298418
it('calls updater on change for a NamedNodeUriField', () => {
299419
const container = document.createElement('div')
300420
const already = {}

0 commit comments

Comments
 (0)