Skip to content

Commit 23b5892

Browse files
authored
chat todos - drop offscreen status element (microsoft#273578)
* chat todos - drop offscreen status element * fix text
1 parent 541d997 commit 23b5892

2 files changed

Lines changed: 28 additions & 30 deletions

File tree

src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ interface ITodoListTemplate {
3232
readonly todoElement: HTMLElement;
3333
readonly statusIcon: HTMLElement;
3434
readonly iconLabel: IconLabel;
35-
readonly statusElement: HTMLElement;
3635
}
3736

3837
class TodoListRenderer implements IListRenderer<IChatTodo, ITodoListTemplate> {
@@ -53,18 +52,12 @@ class TodoListRenderer implements IListRenderer<IChatTodo, ITodoListTemplate> {
5352

5453
const todoContent = dom.append(todoElement, dom.$('.todo-content'));
5554
const iconLabel = templateDisposables.add(new IconLabel(todoContent, { supportIcons: false }));
56-
const statusElement = dom.append(todoContent, dom.$('.todo-status-text'));
57-
statusElement.style.position = 'absolute';
58-
statusElement.style.left = '-10000px';
59-
statusElement.style.width = '1px';
60-
statusElement.style.height = '1px';
61-
statusElement.style.overflow = 'hidden';
62-
63-
return { templateDisposables, todoElement, statusIcon, iconLabel, statusElement };
55+
56+
return { templateDisposables, todoElement, statusIcon, iconLabel };
6457
}
6558

6659
renderElement(todo: IChatTodo, index: number, templateData: ITodoListTemplate): void {
67-
const { todoElement, statusIcon, iconLabel, statusElement } = templateData;
60+
const { todoElement, statusIcon, iconLabel } = templateData;
6861

6962
// Update status icon
7063
statusIcon.className = `todo-status-icon codicon ${this.getStatusIconClass(todo.status)}`;
@@ -75,17 +68,12 @@ class TodoListRenderer implements IListRenderer<IChatTodo, ITodoListTemplate> {
7568
const title = includeDescription && todo.description && todo.description.trim() ? todo.description : undefined;
7669
iconLabel.setLabel(todo.title, undefined, { title });
7770

78-
// Update hidden status text for screen readers
79-
const statusText = this.getStatusText(todo.status);
80-
statusElement.id = `todo-status-${index}`;
81-
statusElement.textContent = statusText;
82-
8371
// Update aria-label
72+
const statusText = this.getStatusText(todo.status);
8473
const ariaLabel = includeDescription && todo.description && todo.description.trim()
8574
? localize('chat.todoList.itemWithDescription', '{0}, {1}, {2}', todo.title, statusText, todo.description)
8675
: localize('chat.todoList.item', '{0}, {1}', todo.title, statusText);
8776
todoElement.setAttribute('aria-label', ariaLabel);
88-
todoElement.setAttribute('aria-describedby', `todo-status-${index}`);
8977
}
9078

9179
disposeTemplate(templateData: ITodoListTemplate): void {

src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,32 @@ suite('ChatTodoListWidget Accessibility', () => {
122122
// Progress is 2/3 because: 1 completed + 1 in-progress (current) = task 2 of 3
123123
assert.ok(titleText?.includes('(2/3)'), `Title should show progress format, but got: "${titleText}"`);
124124
assert.ok(titleText?.includes('Second task'), `Title should show current task when collapsed, but got: "${titleText}"`);
125-
}); test('hidden status text elements exist for screen readers', () => {
125+
});
126+
127+
test('todo items have complete aria-label with status information', () => {
126128
widget.render('test-session');
127129

128-
const statusElements = widget.domNode.querySelectorAll('.todo-status-text');
129-
assert.strictEqual(statusElements.length, 3, 'Should have 3 status text elements');
130-
131-
statusElements.forEach((element, index) => {
132-
assert.strictEqual(element.id, `todo-status-${index}`, 'Should have proper ID');
133-
// Check that it's visually hidden but accessible to screen readers
134-
const style = (element as HTMLElement).style;
135-
assert.strictEqual(style.position, 'absolute');
136-
assert.strictEqual(style.left, '-10000px');
137-
assert.strictEqual(style.width, '1px');
138-
assert.strictEqual(style.height, '1px');
139-
assert.strictEqual(style.overflow, 'hidden');
140-
});
130+
const todoItems = widget.domNode.querySelectorAll('.todo-item');
131+
assert.strictEqual(todoItems.length, 3, 'Should have 3 todo items');
132+
133+
// Check first item (not-started) - aria-label should include title and status
134+
const firstItem = todoItems[0] as HTMLElement;
135+
const firstAriaLabel = firstItem.getAttribute('aria-label');
136+
assert.ok(firstAriaLabel?.includes('First task'), 'First item aria-label should include title');
137+
assert.ok(firstAriaLabel?.includes('not started'), 'First item aria-label should include status');
138+
139+
// Check second item (in-progress with description) - aria-label should include title, status, and description
140+
const secondItem = todoItems[1] as HTMLElement;
141+
const secondAriaLabel = secondItem.getAttribute('aria-label');
142+
assert.ok(secondAriaLabel?.includes('Second task'), 'Second item aria-label should include title');
143+
assert.ok(secondAriaLabel?.includes('in progress'), 'Second item aria-label should include status');
144+
assert.ok(secondAriaLabel?.includes('This is a task description'), 'Second item aria-label should include description');
145+
146+
// Check third item (completed) - aria-label should include title and status
147+
const thirdItem = todoItems[2] as HTMLElement;
148+
const thirdAriaLabel = thirdItem.getAttribute('aria-label');
149+
assert.ok(thirdAriaLabel?.includes('Third task'), 'Third item aria-label should include title');
150+
assert.ok(thirdAriaLabel?.includes('completed'), 'Third item aria-label should include status');
141151
});
142152

143153
test('widget displays properly when no todos exist', () => {

0 commit comments

Comments
 (0)