fix: accessibility issues for builder's design
This commit is contained in:
1 parent
8e344b2500
commit
76e9e1220c
16 files changed
+290
-147
No files matched your search
@@ -173,6 +173,8 @@
|
||||
--ui-warning: var(--ui-color-warning-700);
|
||||
--ui-error: var(--ui-color-error-700);
|
||||
--rc-sand: #e8dfd1;
|
||||
--rc-sel: #e8dfd1;
|
||||
--rc-on-sel: #111111;
|
||||
--rc-on-sand: #111111;
|
||||
--rc-success-soft: #eaefea;
|
||||
--rc-info-soft: #ebeef0;
|
||||
@@ -206,6 +208,8 @@
|
||||
--ui-warning: var(--ui-color-warning-300);
|
||||
--ui-error: var(--ui-color-error-300);
|
||||
--rc-sand: #d4c5b0;
|
||||
--rc-sel: #222222;
|
||||
--rc-on-sel: #fafafa;
|
||||
--rc-on-sand: #111111;
|
||||
--rc-success-soft: #1f241f;
|
||||
--rc-info-soft: #202428;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
kind: 'new' | 'changed' | 'cause' | 'required' | 'unique'
|
||||
onSand?: boolean
|
||||
compact?: boolean
|
||||
}>()
|
||||
|
||||
const badges = {
|
||||
@@ -42,12 +42,10 @@ const badges = {
|
||||
},
|
||||
} as const
|
||||
|
||||
const OUTLINE_ON_SAND = 'border border-(--rc-on-sand) bg-transparent text-(--rc-on-sand)'
|
||||
|
||||
const badge = computed(() => badges[props.kind])
|
||||
const badgeIcon = computed(() => ('icon' in badge.value ? badge.value.icon : undefined))
|
||||
const badgeClass = computed(() =>
|
||||
props.onSand && props.kind !== 'new' ? OUTLINE_ON_SAND : badge.value.class,
|
||||
const badgeLabel = computed(() =>
|
||||
props.compact ? badge.value.label.charAt(0).toUpperCase() : badge.value.label,
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -56,12 +54,12 @@ const badgeClass = computed(() =>
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
:icon="badgeIcon"
|
||||
:class="badgeClass"
|
||||
:class="badge.class"
|
||||
:ui="{
|
||||
base: 'h-5 shrink-0 gap-1 px-1.5 py-0 text-xs font-medium whitespace-nowrap',
|
||||
leadingIcon: 'size-3',
|
||||
}"
|
||||
>
|
||||
{{ badge.label }}
|
||||
{{ badgeLabel }}
|
||||
</UBadge>
|
||||
</template>
|
||||
@@ -32,7 +32,6 @@ const emit = defineEmits<{
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const tipOpen = ref(false)
|
||||
const slugMode = computed(() => keyDisplayMode(props.keyMode, props.keyValue))
|
||||
const hasError = computed(() => Boolean(props.nameError || props.keyError))
|
||||
|
||||
watch(
|
||||
() => props.keyFocusToken,
|
||||
@@ -47,7 +46,7 @@ watch(
|
||||
<div
|
||||
ref="root"
|
||||
class="flex min-w-0 flex-col gap-3 transition-opacity md:flex-row md:gap-4"
|
||||
:class="[hasError ? 'md:items-start' : 'md:items-end', disabled ? 'opacity-60' : '']"
|
||||
:class="[disabled ? 'opacity-60' : '', 'md:items-start']"
|
||||
>
|
||||
<div class="md:w-80">
|
||||
<DirectoryField :label="t('Name')" name="name" required :error="nameError">
|
||||
@@ -93,7 +92,7 @@ watch(
|
||||
</div>
|
||||
</UTooltip>
|
||||
</div>
|
||||
<div v-else :class="keyError ? 'md:w-90' : 'md:w-80'">
|
||||
<div v-else class="md:w-80">
|
||||
<SlugField
|
||||
:label="t('Key')"
|
||||
name="key"
|
||||
@@ -117,7 +116,7 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
<span class="hidden flex-1 md:block" />
|
||||
<div v-if="$slots.actions" :class="hasError ? 'md:mt-5.5' : ''">
|
||||
<div v-if="$slots.actions" class="flex md:mt-6.5 md:h-8 md:items-center">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,8 +118,8 @@ function onChoice(value: unknown) {
|
||||
}
|
||||
|
||||
const SWITCH_UI = {
|
||||
root: 'items-start gap-3',
|
||||
wrapper: 'ms-0',
|
||||
root: 'w-full flex-row-reverse items-center justify-between gap-4 border-b border-default py-3',
|
||||
wrapper: 'ms-0 flex min-w-0 flex-col gap-0.5',
|
||||
label: 'font-medium text-highlighted',
|
||||
description: 'mt-0 text-xs/4.5 text-pretty text-muted',
|
||||
}
|
||||
@@ -186,74 +186,36 @@ watch(
|
||||
@update:model-value="emit('type', $event)"
|
||||
/>
|
||||
|
||||
<div class="grid items-start gap-3.5" :class="gridClass">
|
||||
<div class="flex min-w-0 flex-col gap-3.5">
|
||||
<AppSwitch
|
||||
:model-value="field.required"
|
||||
:label="t('Required')"
|
||||
:description="t('An entry can\'t be saved with this field empty.')"
|
||||
:disabled="disabled"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('required', Boolean($event))"
|
||||
/>
|
||||
<AppSwitch
|
||||
:model-value="canUnique && field.unique"
|
||||
:label="t('Unique values')"
|
||||
:description="uniqueHelp"
|
||||
:disabled="disabled || !canUnique"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('unique', Boolean($event))"
|
||||
/>
|
||||
<AppSwitch
|
||||
v-if="type === 'boolean'"
|
||||
:model-value="field.defaultOn"
|
||||
:label="t('On by default')"
|
||||
:description="defaultOnHelp"
|
||||
:disabled="disabled"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('defaultOn', Boolean($event))"
|
||||
/>
|
||||
<DirectoryField
|
||||
v-else
|
||||
:label="t('Default value')"
|
||||
name="default"
|
||||
:error="errors.default"
|
||||
:help="defaultHelp"
|
||||
>
|
||||
<USelect
|
||||
v-if="type === 'select'"
|
||||
:model-value="choiceValue"
|
||||
:items="choiceItems"
|
||||
:placeholder="t('None')"
|
||||
:disabled="disabled || field.unique"
|
||||
class="w-full"
|
||||
:ui="{ base: 'h-11 text-lg md:h-8 md:text-sm' }"
|
||||
@update:model-value="onChoice"
|
||||
/>
|
||||
<UTextarea
|
||||
v-else-if="type === 'richtext'"
|
||||
:model-value="field.defaultValue"
|
||||
:placeholder="t('No default')"
|
||||
:disabled="disabled || field.unique"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
@update:model-value="emit('default', String($event))"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
:model-value="field.defaultValue"
|
||||
:type="type === 'number' ? 'number' : type === 'date' ? 'date' : 'text'"
|
||||
:placeholder="t('No default')"
|
||||
:disabled="disabled || field.unique"
|
||||
:color="errors.default ? 'error' : undefined"
|
||||
class="w-full"
|
||||
:ui="{ base: 'h-11 text-lg md:h-8 md:text-sm' }"
|
||||
@update:model-value="emit('default', String($event))"
|
||||
/>
|
||||
</DirectoryField>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col border-t border-default">
|
||||
<AppSwitch
|
||||
:model-value="field.required"
|
||||
:label="t('Required')"
|
||||
:description="t('An entry can\'t be saved with this field empty.')"
|
||||
:disabled="disabled"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('required', Boolean($event))"
|
||||
/>
|
||||
<AppSwitch
|
||||
:model-value="canUnique && field.unique"
|
||||
:label="t('Unique values')"
|
||||
:description="uniqueHelp"
|
||||
:disabled="disabled || !canUnique"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('unique', Boolean($event))"
|
||||
/>
|
||||
<AppSwitch
|
||||
v-if="type === 'boolean'"
|
||||
:model-value="field.defaultOn"
|
||||
:label="t('On by default')"
|
||||
:description="defaultOnHelp"
|
||||
:disabled="disabled"
|
||||
:ui="SWITCH_UI"
|
||||
@update:model-value="emit('defaultOn', Boolean($event))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasLimits" class="flex min-w-0 flex-col gap-3.5">
|
||||
<div class="grid items-start gap-3.5" :class="gridClass">
|
||||
<template v-if="hasLimits">
|
||||
<DirectoryField :label="limitLabels[0]" name="min" :error="errors.min">
|
||||
<UInput
|
||||
:model-value="field.min"
|
||||
@@ -291,8 +253,46 @@ watch(
|
||||
</template>
|
||||
</UInput>
|
||||
</DirectoryField>
|
||||
</div>
|
||||
<div v-else class="flex min-w-0 flex-col gap-1.5">
|
||||
</template>
|
||||
<DirectoryField
|
||||
v-if="type !== 'boolean'"
|
||||
:label="t('Default value')"
|
||||
name="default"
|
||||
:error="errors.default"
|
||||
:help="defaultHelp"
|
||||
>
|
||||
<USelect
|
||||
v-if="type === 'select'"
|
||||
:model-value="choiceValue"
|
||||
:items="choiceItems"
|
||||
:placeholder="t('None')"
|
||||
:disabled="disabled || field.unique"
|
||||
class="w-full"
|
||||
:ui="{ base: 'h-11 text-lg md:h-8 md:text-sm' }"
|
||||
@update:model-value="onChoice"
|
||||
/>
|
||||
<UTextarea
|
||||
v-else-if="type === 'richtext'"
|
||||
:model-value="field.defaultValue"
|
||||
:placeholder="t('No default')"
|
||||
:disabled="disabled || field.unique"
|
||||
:rows="3"
|
||||
class="w-full"
|
||||
@update:model-value="emit('default', String($event))"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
:model-value="field.defaultValue"
|
||||
:type="type === 'number' ? 'number' : type === 'date' ? 'date' : 'text'"
|
||||
:placeholder="t('No default')"
|
||||
:disabled="disabled || field.unique"
|
||||
:color="errors.default ? 'error' : undefined"
|
||||
class="w-full"
|
||||
:ui="{ base: 'h-11 text-lg md:h-8 md:text-sm' }"
|
||||
@update:model-value="emit('default', String($event))"
|
||||
/>
|
||||
</DirectoryField>
|
||||
<div v-if="!hasLimits" class="flex min-w-0 flex-col gap-1.5">
|
||||
<span class="font-medium text-highlighted">{{ t('Limits') }}</span>
|
||||
<span class="text-xs/4.5 text-muted">{{ noLimitsText }}</span>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,7 @@ function onFocusin(event: FocusEvent) {
|
||||
@down="onMove(field.uid, 1)"
|
||||
@remove="emit('remove', field.uid)"
|
||||
@grip-keydown="reorder.onKeydown($event, index)"
|
||||
@grip-pointerdown="reorder.onPointerdown($event, index)"
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
|
||||
@@ -27,6 +27,7 @@ defineEmits<{
|
||||
down: []
|
||||
remove: []
|
||||
'grip-keydown': [event: KeyboardEvent]
|
||||
'grip-pointerdown': [event: PointerEvent]
|
||||
}>()
|
||||
|
||||
const name = computed(() => props.field.label.trim() || t('Untitled field'))
|
||||
@@ -35,7 +36,7 @@ const key = computed(() => props.field.key || '–')
|
||||
const rowClass = computed(() => [
|
||||
props.invalid ? 'border-error' : props.grabbed ? 'border-primary' : 'border-default',
|
||||
props.grabbed ? 'shadow-(--shadow-popover) outline-2 -outline-offset-1 outline-primary' : '',
|
||||
props.selected ? 'bg-(--rc-sand) text-(--rc-on-sand)' : 'bg-default text-highlighted',
|
||||
props.selected ? 'bg-(--rc-sel) text-(--rc-on-sel)' : 'bg-default text-highlighted',
|
||||
props.disabled ? 'opacity-60' : '',
|
||||
])
|
||||
</script>
|
||||
@@ -56,9 +57,10 @@ const rowClass = computed(() => [
|
||||
:aria-pressed="grabbed"
|
||||
:disabled="disabled"
|
||||
:tabindex="tabbable ? 0 : -1"
|
||||
class="flex h-8 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:outline-primary max-md:h-11 max-md:w-9"
|
||||
:class="selected ? 'text-(--rc-on-sand)' : 'text-muted'"
|
||||
class="flex h-8 w-6 shrink-0 cursor-grab touch-none items-center justify-center focus-visible:outline-2 focus-visible:outline-primary max-md:h-11 max-md:w-9"
|
||||
:class="selected ? 'text-(--rc-on-sel)' : 'text-muted'"
|
||||
@keydown="$emit('grip-keydown', $event)"
|
||||
@pointerdown="$emit('grip-pointerdown', $event)"
|
||||
@click.stop
|
||||
>
|
||||
<UIcon name="i-lucide-grip-vertical" class="size-4" />
|
||||
@@ -81,27 +83,43 @@ const rowClass = computed(() => [
|
||||
</span>
|
||||
<span class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate font-medium" data-testid="field-row-label">{{ name }}</span>
|
||||
<span
|
||||
class="truncate font-mono text-xs"
|
||||
:class="selected ? 'text-(--rc-on-sand)' : 'text-muted'"
|
||||
data-testid="field-row-key"
|
||||
>{{ key }}</span
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<span
|
||||
class="min-w-0 truncate font-mono text-xs"
|
||||
:class="selected ? 'text-(--rc-on-sel)' : 'text-muted'"
|
||||
data-testid="field-row-key"
|
||||
>{{ key }}</span
|
||||
>
|
||||
<span class="flex shrink-0 items-center gap-1.5" data-testid="field-row-badges">
|
||||
<BuilderBadge v-if="marks.isNew" kind="new" />
|
||||
<BuilderBadge v-if="marks.changed" kind="changed" />
|
||||
<BuilderBadge v-if="marks.cause && !selected" kind="cause" />
|
||||
<span
|
||||
v-if="field.required"
|
||||
class="flex shrink-0"
|
||||
:title="t('Required')"
|
||||
:aria-label="t('Required')"
|
||||
>
|
||||
<BuilderBadge kind="required" compact />
|
||||
</span>
|
||||
<span
|
||||
v-if="field.unique"
|
||||
class="flex shrink-0"
|
||||
:title="t('Unique')"
|
||||
:aria-label="t('Unique')"
|
||||
>
|
||||
<BuilderBadge kind="unique" compact />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<span class="flex shrink-0 items-center gap-1" data-testid="field-row-badges">
|
||||
<BuilderBadge v-if="marks.isNew" kind="new" />
|
||||
<BuilderBadge v-if="marks.changed" kind="changed" />
|
||||
<BuilderBadge v-if="marks.cause" kind="cause" />
|
||||
<BuilderBadge v-if="field.required" kind="required" :on-sand="selected" />
|
||||
<BuilderBadge v-if="field.unique" kind="unique" :on-sand="selected" />
|
||||
<UIcon
|
||||
v-if="invalid"
|
||||
name="i-lucide-circle-alert"
|
||||
data-testid="field-row-alert"
|
||||
class="size-4 text-error"
|
||||
/>
|
||||
</span>
|
||||
<UIcon
|
||||
v-if="invalid && !selected"
|
||||
name="i-lucide-circle-alert"
|
||||
data-testid="field-row-alert"
|
||||
class="mx-1 size-4 shrink-0 text-error"
|
||||
/>
|
||||
<FieldActions
|
||||
v-if="selected && showActions"
|
||||
:first="index === 0"
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('FieldInspector', () => {
|
||||
)
|
||||
|
||||
const alerts = [...document.body.querySelectorAll('[role="alert"]')].map(text)
|
||||
expect(alerts).toEqual(['Give the field a label.', 'Bad key.', 'Outside.', 'Order.'])
|
||||
expect(alerts).toEqual(['Give the field a label.', 'Bad key.', 'Order.', 'Outside.'])
|
||||
})
|
||||
|
||||
it('lays the controls out in one column when compact', async () => {
|
||||
|
||||
@@ -138,4 +138,72 @@ describe('FieldList', () => {
|
||||
|
||||
expect(document.body.querySelector('[data-action]')).toBeNull()
|
||||
})
|
||||
|
||||
describe('dragging a grip', () => {
|
||||
function layout() {
|
||||
;[...document.body.querySelectorAll('li[data-uid]')].forEach((row, index) => {
|
||||
row.getBoundingClientRect = () => ({ bottom: (index + 1) * 60 }) as DOMRect
|
||||
})
|
||||
}
|
||||
|
||||
function pointer(target: EventTarget, type: string, clientY = 0, button = 0) {
|
||||
const event = new MouseEvent(type, { bubbles: true, cancelable: true, clientY, button })
|
||||
Object.defineProperty(event, 'pointerType', { value: 'mouse' })
|
||||
target.dispatchEvent(event)
|
||||
}
|
||||
|
||||
it('drops the row where the pointer is released and emits the order once', async () => {
|
||||
await mountInApp(FieldList, props())
|
||||
layout()
|
||||
|
||||
pointer(grip('Title'), 'pointerdown', 10)
|
||||
await flushPromises()
|
||||
pointer(window, 'pointermove', 100)
|
||||
await flushPromises()
|
||||
expect(labels()).toEqual(['Starts on', 'Title', 'Ends on'])
|
||||
layout()
|
||||
pointer(window, 'pointermove', 170)
|
||||
await flushPromises()
|
||||
pointer(window, 'pointerup', 170)
|
||||
await flushPromises()
|
||||
|
||||
expect(emitted('reorder')).toEqual([[[2, 3, 1]]])
|
||||
})
|
||||
|
||||
it('emits nothing when it is released where it started', async () => {
|
||||
await mountInApp(FieldList, props())
|
||||
layout()
|
||||
|
||||
pointer(grip('Title'), 'pointerdown', 10)
|
||||
pointer(window, 'pointerup', 10)
|
||||
await flushPromises()
|
||||
|
||||
expect(emitted('reorder')).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores a secondary button', async () => {
|
||||
await mountInApp(FieldList, props())
|
||||
layout()
|
||||
|
||||
pointer(grip('Title'), 'pointerdown', 10, 2)
|
||||
pointer(window, 'pointermove', 100)
|
||||
await flushPromises()
|
||||
|
||||
expect(labels()).toEqual(['Title', 'Starts on', 'Ends on'])
|
||||
})
|
||||
|
||||
it('puts the rows back when the drag is cancelled', async () => {
|
||||
await mountInApp(FieldList, props())
|
||||
layout()
|
||||
|
||||
pointer(grip('Title'), 'pointerdown', 10)
|
||||
pointer(window, 'pointermove', 100)
|
||||
await flushPromises()
|
||||
pointer(window, 'pointercancel')
|
||||
await flushPromises()
|
||||
|
||||
expect(labels()).toEqual(['Title', 'Starts on', 'Ends on'])
|
||||
expect(emitted('reorder')).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -98,6 +98,59 @@ export function useKeyboardReorder<T>(options: KeyboardReorderOptions<T>) {
|
||||
focusHandle(index)
|
||||
}
|
||||
|
||||
let settling = false
|
||||
|
||||
function rowIndexAt(y: number) {
|
||||
const rows = Array.from(options.root()?.children ?? [])
|
||||
if (!rows.length) {
|
||||
return null
|
||||
}
|
||||
const found = rows.findIndex((row) => y < row.getBoundingClientRect().bottom)
|
||||
return found === -1 ? rows.length - 1 : found
|
||||
}
|
||||
|
||||
async function onPointermove(event: PointerEvent) {
|
||||
const current = grabbed.value
|
||||
const target = rowIndexAt(event.clientY)
|
||||
if (settling || current === null || target === null || target === current) {
|
||||
return
|
||||
}
|
||||
settling = true
|
||||
move(target - current)
|
||||
await nextTick()
|
||||
settling = false
|
||||
}
|
||||
|
||||
function stopPointer() {
|
||||
settling = false
|
||||
document.body.style.userSelect = ''
|
||||
window.removeEventListener('pointermove', onPointermove)
|
||||
window.removeEventListener('pointerup', onPointerup)
|
||||
window.removeEventListener('pointercancel', onPointercancel)
|
||||
}
|
||||
|
||||
function onPointerup() {
|
||||
stopPointer()
|
||||
drop()
|
||||
}
|
||||
|
||||
function onPointercancel() {
|
||||
stopPointer()
|
||||
cancel()
|
||||
}
|
||||
|
||||
function onPointerdown(event: PointerEvent, index: number) {
|
||||
if (grabbed.value !== null || (event.pointerType === 'mouse' && event.button !== 0)) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
grab(index)
|
||||
document.body.style.userSelect = 'none'
|
||||
window.addEventListener('pointermove', onPointermove)
|
||||
window.addEventListener('pointerup', onPointerup)
|
||||
window.addEventListener('pointercancel', onPointercancel)
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent, index: number) {
|
||||
if (grabbed.value === null) {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
@@ -121,5 +174,5 @@ export function useKeyboardReorder<T>(options: KeyboardReorderOptions<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
return { shown, grabbed, active, announcement, onKeydown }
|
||||
return { shown, grabbed, active, announcement, onKeydown, onPointerdown }
|
||||
}
|
||||
@@ -318,13 +318,15 @@ describe('Field errors (design state 5)', () => {
|
||||
|
||||
expect(chip()).toBe('3 problems to fix')
|
||||
expect(saveButton()!.disabled).toBe(true)
|
||||
expect(all('[data-testid="field-row-alert"]')).toHaveLength(3)
|
||||
expect(all('[data-testid="field-row-alert"]')).toHaveLength(2)
|
||||
expect(text(inspector())).toContain("Choices can't be blank.")
|
||||
expect(text(inspector())).toContain('Two choices are the same.')
|
||||
await selectRow('Capacity')
|
||||
expect(text(inspector())).toContain("Lowest value can't be above the highest value.")
|
||||
await selectRow('Untitled field')
|
||||
expect(text(inspector())).toContain('Give the field a label.')
|
||||
await selectRow('Title')
|
||||
expect(all('[data-testid="field-row-alert"]')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('shows a single key error for a new field and none for a saved field', async () => {
|
||||
|
||||
@@ -650,6 +650,7 @@ describe('Refused (design state 15 and group 3, state 3)', () => {
|
||||
await click(button('Apply changes'))
|
||||
await click(button('Back to editing'))
|
||||
await vi.waitFor(() => expect(dialog()).toBeNull())
|
||||
await selectRow('Ends on')
|
||||
expect(badgesOf('Capacity')).toContain('Possible cause')
|
||||
|
||||
answer = (body) => updatedType('events', body)
|
||||
|
||||
@@ -187,6 +187,7 @@ describe('When errors are shown', () => {
|
||||
await typeInto(el<HTMLInputElement>('input[name="label"]'), 'x')
|
||||
await typeInto(el<HTMLInputElement>('input[name="label"]'), '')
|
||||
|
||||
await click(rows()[0]!.querySelector('[data-field-row]'))
|
||||
const invalid = rows().filter((row) => row.querySelector('[data-testid="field-row-alert"]'))
|
||||
expect(invalid).toHaveLength(1)
|
||||
expect(invalid[0]!.className).toContain('border-error')
|
||||
|
||||
@@ -81,12 +81,10 @@ describe('Field list rows', () => {
|
||||
await addField('Second')
|
||||
|
||||
const first = rows()[0]!
|
||||
expect(text(first.querySelector('[data-testid="field-row-badges"]'))).toBe(
|
||||
'New Required Unique',
|
||||
)
|
||||
expect(text(first.querySelector('[data-testid="field-row-badges"]'))).toBe('New R U')
|
||||
expect(first.querySelector('[data-action]')).toBeNull()
|
||||
const selected = rows()[1]!
|
||||
expect(selected.className).toContain('bg-(--rc-sand)')
|
||||
expect(selected.className).toContain('bg-(--rc-sel)')
|
||||
expect(
|
||||
[...selected.querySelectorAll('[data-action]')].map((node) =>
|
||||
node.getAttribute('aria-label'),
|
||||
|
||||
Reference in new issue
Block a user