fix: label image actions by item
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-13 17:55:02 -04:00
parent a81f882831
commit 3132e80646
3 changed files with 13 additions and 12 deletions
@@ -38,11 +38,11 @@ export const EditAltTextWithKeyboard = {
args: { images }, args: { images },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await userEvent.click(canvas.getAllByRole('button', { name: 'Edit alt text' })[0]); await userEvent.click(canvas.getAllByRole('button', { name: /Edit alt text for image/ })[0]);
const input = canvas.getAllByLabelText('Alt text')[0]; const input = canvas.getAllByLabelText('Alt text')[0];
await userEvent.clear(input); await userEvent.clear(input);
await userEvent.type(input, 'Updated front image'); await userEvent.type(input, 'Updated front image');
await userEvent.click(canvas.getByRole('button', { name: 'Save alt text' })); await userEvent.click(canvas.getByRole('button', { name: /Save alt text for image/ }));
await expect(canvas.getByDisplayValue('Updated front image')).toBeInTheDocument(); await expect(canvas.getByDisplayValue('Updated front image')).toBeInTheDocument();
}, },
}; };
@@ -60,7 +60,7 @@ export const DisabledAfterRefreshFailure = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await expect(canvas.getByLabelText('Add images')).toBeDisabled(); await expect(canvas.getByLabelText('Add images')).toBeDisabled();
for (const button of canvas.getAllByRole('button', { name: /Edit alt text|Make primary|Move up|Move down|Delete image/ })) { for (const button of canvas.getAllByRole('button', { name: /Edit alt text for image|Make image .* primary|Move image .* (up|down)|Delete image/ })) {
await expect(button).toBeDisabled(); await expect(button).toBeDisabled();
} }
}, },
@@ -70,7 +70,7 @@ export const AltTextLimit = {
args: { images: [{ ...images[0], altText: 'card '.repeat(200) }] }, args: { images: [{ ...images[0], altText: 'card '.repeat(200) }] },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button', { name: 'Edit alt text' })); await userEvent.click(canvas.getByRole('button', { name: /Edit alt text for image/ }));
const input = canvas.getByLabelText('Alt text'); const input = canvas.getByLabelText('Alt text');
await expect(input).toHaveAttribute('maxlength', '1000'); await expect(input).toHaveAttribute('maxlength', '1000');
await expect(input).toHaveAccessibleDescription(/up to 1,000 characters/); await expect(input).toHaveAccessibleDescription(/up to 1,000 characters/);
@@ -86,7 +86,7 @@ export const OversizedExistingAltText = {
args: { images: [{ ...images[0], altText: 'card '.repeat(201) }] }, args: { images: [{ ...images[0], altText: 'card '.repeat(201) }] },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button', { name: 'Edit alt text' })); await userEvent.click(canvas.getByRole('button', { name: /Edit alt text for image/ }));
await expect(canvas.getByRole('alert')).toHaveTextContent('Alt text must be 1,000 characters or fewer.'); await expect(canvas.getByRole('alert')).toHaveTextContent('Alt text must be 1,000 characters or fewer.');
await expect(canvas.getByLabelText('Alt text')).toHaveAttribute('aria-invalid', 'true'); await expect(canvas.getByLabelText('Alt text')).toHaveAttribute('aria-invalid', 'true');
await expect(canvas.getByRole('button', { name: 'Save alt text' })).toBeDisabled(); await expect(canvas.getByRole('button', { name: 'Save alt text' })).toBeDisabled();
@@ -90,19 +90,19 @@ function handleDrop(event) {
<div class="admin-image-manager__row"> <div class="admin-image-manager__row">
<label class="fixture-label" :for="`admin-alt-text-${image.id}`">Alt text</label> <label class="fixture-label" :for="`admin-alt-text-${image.id}`">Alt text</label>
<input :id="`admin-alt-text-${image.id}`" v-model="altTextDrafts[image.id]" class="fixture-input" maxlength="1000" :aria-describedby="`admin-alt-help-${image.id}${(altTextDrafts[image.id] ?? '').trim().length > 1000 ? ` admin-alt-error-${image.id}` : ''}`" :aria-invalid="(altTextDrafts[image.id] ?? '').trim().length > 1000" :disabled="isSaving || disabled || editingImageId !== image.id" /> <input :id="`admin-alt-text-${image.id}`" v-model="altTextDrafts[image.id]" class="fixture-input" maxlength="1000" :aria-describedby="`admin-alt-help-${image.id}${(altTextDrafts[image.id] ?? '').trim().length > 1000 ? ` admin-alt-error-${image.id}` : ''}`" :aria-invalid="(altTextDrafts[image.id] ?? '').trim().length > 1000" :disabled="isSaving || disabled || editingImageId !== image.id" />
<button v-if="editingImageId !== image.id" class="button button--secondary" type="button" :disabled="isSaving || disabled" @click="beginEditAltText(image)">Edit alt text</button> <button v-if="editingImageId !== image.id" class="button button--secondary" type="button" :aria-label="`Edit alt text for image ${index + 1}`" :disabled="isSaving || disabled" @click="beginEditAltText(image)">Edit alt text</button>
<button v-else class="button button--primary" type="button" :disabled="isSaving || disabled || (altTextDrafts[image.id] ?? '').trim().length > 1000" @click="saveAltText(image.id)">Save alt text</button> <button v-else class="button button--primary" type="button" :aria-label="`Save alt text for image ${index + 1}`" :disabled="isSaving || disabled || (altTextDrafts[image.id] ?? '').trim().length > 1000" @click="saveAltText(image.id)">Save alt text</button>
</div> </div>
<p :id="`admin-alt-help-${image.id}`">Describe the image in up to 1,000 characters. Clear the field to remove its alt text.</p> <p :id="`admin-alt-help-${image.id}`">Describe the image in up to 1,000 characters. Clear the field to remove its alt text.</p>
<p :id="`admin-alt-error-${image.id}`" v-if="(altTextDrafts[image.id] ?? '').trim().length > 1000" class="state state--error" role="alert">Alt text must be 1,000 characters or fewer.</p> <p :id="`admin-alt-error-${image.id}`" v-if="(altTextDrafts[image.id] ?? '').trim().length > 1000" class="state state--error" role="alert">Alt text must be 1,000 characters or fewer.</p>
<div class="admin-image-manager__row"> <div class="admin-image-manager__row">
<button class="button button--secondary" type="button" :disabled="isSaving || disabled || image.isPrimary" @click="emit('primary', image.id)">Make primary</button> <button class="button button--secondary" type="button" :aria-label="`Make image ${index + 1} primary`" :disabled="isSaving || disabled || image.isPrimary" @click="emit('primary', image.id)">Make primary</button>
<button class="button button--secondary" type="button" :disabled="isSaving || disabled || index === 0" @click="emit('move', { id: image.id, direction: 'up' })">Move up</button> <button class="button button--secondary" type="button" :aria-label="`Move image ${index + 1} up`" :disabled="isSaving || disabled || index === 0" @click="emit('move', { id: image.id, direction: 'up' })">Move up</button>
<button class="button button--secondary" type="button" :disabled="isSaving || disabled || index === orderedImages.length - 1" @click="emit('move', { id: image.id, direction: 'down' })">Move down</button> <button class="button button--secondary" type="button" :aria-label="`Move image ${index + 1} down`" :disabled="isSaving || disabled || index === orderedImages.length - 1" @click="emit('move', { id: image.id, direction: 'down' })">Move down</button>
<button v-if="image.processingStatus === 'FAILED'" class="button button--secondary" type="button" :disabled="isSaving || disabled" @click="emit('retry', image.id)">Retry processing</button> <button v-if="image.processingStatus === 'FAILED'" class="button button--secondary" type="button" :aria-label="`Retry processing for image ${index + 1}`" :disabled="isSaving || disabled" @click="emit('retry', image.id)">Retry processing</button>
<button class="button button--danger" type="button" :disabled="isSaving || disabled" @click="emit('delete', image.id)">Delete image</button> <button class="button button--danger" type="button" :aria-label="`Delete image ${index + 1}`" :disabled="isSaving || disabled" @click="emit('delete', image.id)">Delete image</button>
</div> </div>
</li> </li>
</ul> </ul>
+1
View File
@@ -285,6 +285,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
- Review/list controls now explain active-copy requirements, cents units, and publication prerequisites through associated help text. - Review/list controls now explain active-copy requirements, cents units, and publication prerequisites through associated help text.
- Image upload controls now expose accepted formats and the configured per-item limit through associated help text. - Image upload controls now expose accepted formats and the configured per-item limit through associated help text.
- Image upload constraint guidance now has Storybook interaction coverage. - Image upload constraint guidance now has Storybook interaction coverage.
- Repeated image-management actions now include image-specific accessible names.
## Local Commands ## Local Commands