Skip to content

Commit ce57bda

Browse files
authored
Merge pull request #684 from ProgressPlanner/ari/a11y-fixes-2025-10-24
A11y fixes and improvements for v1.9.0
2 parents 75f83fa + 64d9dfa commit ce57bda

17 files changed

Lines changed: 166 additions & 42 deletions

assets/css/admin.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@
2424
--prpl-font-size-6xl: 4.5rem; /* 72px */
2525
}
2626

27+
/*------------------------------------*\
28+
Accessibility: Skip link
29+
\*------------------------------------*/
30+
.prpl-skip-link {
31+
position: absolute;
32+
top: -40px;
33+
left: 0;
34+
background: var(--prpl-color-button-primary);
35+
color: var(--prpl-color-button-primary-text);
36+
padding: 8px 16px;
37+
text-decoration: none;
38+
border-radius: var(--prpl-border-radius);
39+
z-index: 100000;
40+
}
41+
42+
.prpl-skip-link:focus {
43+
top: 10px;
44+
left: 10px;
45+
}
46+
2747
/*------------------------------------*\
2848
Progress Planner dashboard page.
2949
\*------------------------------------*/

assets/js/tour.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,8 @@ function prplStartTour() {
101101
.replace( 'show-tour=true', '' )
102102
);
103103
}
104+
105+
// Add event listener for tour button.
106+
document
107+
.getElementById( 'prpl-start-tour-icon-button' )
108+
?.addEventListener( 'click', prplStartTour );

assets/js/web-components/prpl-tooltip.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ customElements.define(
3636
const tooltipContent = document.createElement( 'div' );
3737
tooltipContent.className = 'prpl-tooltip';
3838
tooltipContent.setAttribute( 'data-tooltip-content', '' );
39+
tooltipContent.setAttribute( 'role', 'tooltip' );
40+
tooltipContent.setAttribute( 'aria-hidden', 'true' );
41+
// Generate a unique ID for the tooltip.
42+
const tooltipId =
43+
'prpl-tooltip-' + Math.random().toString( 36 ).substr( 2, 9 );
44+
tooltipContent.setAttribute( 'id', tooltipId );
3945

4046
// Move content inside the tooltip container
4147
while ( contentSlot?.childNodes.length ) {
@@ -61,6 +67,8 @@ customElements.define(
6167

6268
// Add data attribute to the open button.
6369
openButton.setAttribute( 'data-tooltip-action', 'open-tooltip' );
70+
// Connect button to tooltip for screen readers.
71+
openButton.setAttribute( 'aria-describedby', tooltipId );
6472

6573
openSlot?.remove(); // Remove slot element
6674
openIconSlot?.remove(); // Remove slot element
@@ -108,16 +116,20 @@ customElements.define(
108116

109117
// Open the tooltip.
110118
openTooltipButton?.addEventListener( 'click', () => {
111-
thisObj
112-
.querySelector( '[data-tooltip-content]' )
113-
.setAttribute( 'data-tooltip-visible', 'true' );
119+
const tooltip = thisObj.querySelector(
120+
'[data-tooltip-content]'
121+
);
122+
tooltip.setAttribute( 'data-tooltip-visible', 'true' );
123+
tooltip.removeAttribute( 'aria-hidden' );
114124
} );
115125

116126
// Close the tooltip.
117127
closeTooltipButton?.addEventListener( 'click', () => {
118-
thisObj
119-
.querySelector( '[data-tooltip-content]' )
120-
.removeAttribute( 'data-tooltip-visible' );
128+
const tooltip = thisObj.querySelector(
129+
'[data-tooltip-content]'
130+
);
131+
tooltip.removeAttribute( 'data-tooltip-visible' );
132+
tooltip.setAttribute( 'aria-hidden', 'true' );
121133
} );
122134
};
123135
}

assets/js/widgets/todo.js

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/* global prplSuggestedTask, prplTerms */
1+
/* global prplSuggestedTask, prplTerms, prplL10n */
22
/*
33
* Widget: Todo
44
*
55
* A widget that displays a todo list.
66
*
7-
* Dependencies: wp-api, progress-planner/suggested-task, wp-util, wp-a11y, progress-planner/grid-masonry, progress-planner/celebrate, progress-planner/suggested-task-terms
7+
* Dependencies: wp-api, progress-planner/suggested-task, wp-util, wp-a11y, progress-planner/grid-masonry, progress-planner/celebrate, progress-planner/suggested-task-terms, progress-planner/l10n
88
*/
99

1010
const prplTodoWidget = {
@@ -160,6 +160,11 @@ const prplTodoWidget = {
160160
// Remove the loader.
161161
prplTodoWidget.removeLoader();
162162

163+
// Announce to screen readers.
164+
prplTodoWidget.announceToScreenReader(
165+
prplL10n( 'taskAddedSuccessfully' )
166+
);
167+
163168
// Resize the grid items.
164169
window.dispatchEvent(
165170
new CustomEvent( 'prpl/grid/resize' )
@@ -174,12 +179,40 @@ const prplTodoWidget = {
174179
} );
175180
},
176181

182+
/**
183+
* Announce to screen readers.
184+
*
185+
* @param {string} message The message to announce.
186+
* @param {string} priority The priority ('polite' or 'assertive').
187+
*/
188+
announceToScreenReader: ( message, priority = 'polite' ) => {
189+
// Use WordPress a11y speak if available.
190+
if ( 'undefined' !== typeof wp && wp.a11y && wp.a11y.speak ) {
191+
wp.a11y.speak( message, priority );
192+
} else {
193+
// Fallback to ARIA live region.
194+
const liveRegion = document.getElementById(
195+
'todo-aria-live-region'
196+
);
197+
if ( liveRegion ) {
198+
liveRegion.textContent = message;
199+
setTimeout( () => {
200+
liveRegion.textContent = '';
201+
}, 1000 );
202+
}
203+
}
204+
},
205+
177206
/**
178207
* Add the loader.
179208
*/
180209
addLoader: () => {
181210
const loader = document.createElement( 'span' );
211+
const loadingTasksText = prplL10n( 'loadingTasks' );
182212
loader.className = 'prpl-loader';
213+
loader.setAttribute( 'role', 'status' );
214+
loader.setAttribute( 'aria-live', 'polite' );
215+
loader.innerHTML = `<span class="screen-reader-text">${ loadingTasksText }</span>`;
183216
document.getElementById( 'todo-list' ).appendChild( loader );
184217
},
185218

@@ -220,12 +253,25 @@ const prplTodoWidget = {
220253
* Delete all completed tasks.
221254
*/
222255
deleteAllCompletedTasks: () => {
223-
document
224-
.querySelectorAll( '#todo-list-completed .prpl-suggested-task' )
225-
.forEach( ( item ) => {
226-
const postId = parseInt( item.getAttribute( 'data-post-id' ) );
227-
prplSuggestedTask.trash( postId );
228-
} );
256+
const items = document.querySelectorAll(
257+
'#todo-list-completed .prpl-suggested-task'
258+
);
259+
const itemCount = items.length;
260+
261+
items.forEach( ( item ) => {
262+
const postId = parseInt( item.getAttribute( 'data-post-id' ) );
263+
prplSuggestedTask.trash( postId );
264+
} );
265+
266+
// Announce to screen readers.
267+
const tasksWord =
268+
itemCount === 1
269+
? prplL10n( 'taskDeleted' )
270+
: prplL10n( 'tasksDeleted' );
271+
prplTodoWidget.announceToScreenReader(
272+
`${ itemCount } ${ tasksWord }`,
273+
'assertive'
274+
);
229275

230276
// Resize event will be triggered by the trash function.
231277
},
@@ -237,6 +283,27 @@ document
237283
window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) );
238284
} );
239285

286+
// Add event listener for delete all button.
287+
document
288+
.getElementById( 'todo-list-completed-delete-all' )
289+
?.addEventListener( 'click', () => {
290+
prplTodoWidget.showDeleteAllPopover();
291+
} );
292+
293+
// Add event listener for cancel button in delete all popover.
294+
document
295+
.getElementById( 'todo-list-completed-delete-all-cancel' )
296+
?.addEventListener( 'click', () => {
297+
prplTodoWidget.closeDeleteAllPopover();
298+
} );
299+
300+
// Add event listener for confirm button in delete all popover.
301+
document
302+
.getElementById( 'todo-list-completed-delete-all-confirm' )
303+
?.addEventListener( 'click', () => {
304+
prplTodoWidget.deleteAllCompletedTasksAndClosePopover();
305+
} );
306+
240307
document.addEventListener( 'prpl/suggestedTask/itemInjected', ( event ) => {
241308
if ( 'todo-list' !== event.detail.listId ) {
242309
return;

classes/admin/class-enqueue.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,13 @@ public function get_localized_strings() {
402402
'showAllRecommendations' => \esc_html__( 'Show all recommendations', 'progress-planner' ),
403403
'showFewerRecommendations' => \esc_html__( 'Show fewer recommendations', 'progress-planner' ),
404404
'loadingTasks' => \esc_html__( 'Loading tasks...', 'progress-planner' ),
405+
'taskAddedSuccessfully' => \esc_html__( 'Task added successfully', 'progress-planner' ),
406+
'tasksDeleted' => \esc_html__( 'Completed tasks deleted', 'progress-planner' ),
407+
'taskDeleted' => \esc_html__( 'Completed task deleted', 'progress-planner' ),
408+
'moveUp' => \esc_html__( 'Move up', 'progress-planner' ),
409+
'moveDown' => \esc_html__( 'Move down', 'progress-planner' ),
410+
/* translators: %d: The number of points. */
411+
'fixThisIssue' => \esc_html__( 'Fix this issue for %d points', 'progress-planner' ),
405412
];
406413
}
407414

classes/admin/widgets/class-todo.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ public function the_todo_list() {
5050

5151
<form id="create-todo-item">
5252
<input type="text" id="new-todo-content" placeholder="<?php \esc_attr_e( 'Add a new task', 'progress-planner' ); ?>" aria-label="<?php \esc_attr_e( 'Add a new task', 'progress-planner' ); ?>" required />
53-
<button type="submit" title="<?php \esc_attr_e( 'Add', 'progress-planner' ); ?>">
54-
<span class="dashicons dashicons-plus-alt2"></span>
53+
<button type="submit" aria-label="<?php \esc_attr_e( 'Add task', 'progress-planner' ); ?>">
54+
<span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
55+
<span class="screen-reader-text"><?php \esc_html_e( 'Add task', 'progress-planner' ); ?></span>
5556
</button>
5657
</form>
5758
<details id="todo-list-completed-details">
@@ -63,7 +64,7 @@ public function the_todo_list() {
6364
</span>
6465
</summary>
6566
<div id="todo-list-completed-delete-all-wrapper">
66-
<button id="todo-list-completed-delete-all" onclick="prplTodoWidget.showDeleteAllPopover()">
67+
<button id="todo-list-completed-delete-all">
6768
<span style="display: inline-block; width: 18px; height: 18px;">
6869
<svg role="img" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#9ca3af" d="M32.99 47.88H15.01c-3.46 0-6.38-2.7-6.64-6.15L6.04 11.49l-.72.12c-.82.14-1.59-.41-1.73-1.22-.14-.82.41-1.59 1.22-1.73.79-.14 1.57-.26 2.37-.38h.02c2.21-.33 4.46-.6 6.69-.81v-.72c0-3.56 2.74-6.44 6.25-6.55 2.56-.08 5.15-.08 7.71 0 3.5.11 6.25 2.99 6.25 6.55v.72c2.24.2 4.48.47 6.7.81.79.12 1.59.25 2.38.39.82.14 1.36.92 1.22 1.73-.14.82-.92 1.36-1.73 1.22l-.72-.12-2.33 30.24c-.27 3.45-3.18 6.15-6.64 6.15Zm-17.98-3h17.97c1.9 0 3.51-1.48 3.65-3.38l2.34-30.46c-2.15-.3-4.33-.53-6.48-.7h-.03c-5.62-.43-11.32-.43-16.95 0h-.03c-2.15.17-4.33.4-6.48.7l2.34 30.46c.15 1.9 1.75 3.38 3.65 3.38ZM24 7.01c2.37 0 4.74.07 7.11.22v-.49c0-1.93-1.47-3.49-3.34-3.55-2.5-.08-5.03-.08-7.52 0-1.88.06-3.34 1.62-3.34 3.55v.49c2.36-.15 4.73-.22 7.11-.22Zm5.49 32.26h-.06c-.83-.03-1.47-.73-1.44-1.56l.79-20.65c.03-.83.75-1.45 1.56-1.44.83.03 1.47.73 1.44 1.56l-.79 20.65c-.03.81-.7 1.44-1.5 1.44Zm-10.98 0c-.8 0-1.47-.63-1.5-1.44l-.79-20.65c-.03-.83.61-1.52 1.44-1.56.84 0 1.52.61 1.56 1.44l.79 20.65c.03.83-.61 1.52-1.44 1.56h-.06Z"></path></svg>
6970
</span>
@@ -83,13 +84,13 @@ public function the_todo_list() {
8384
</div>
8485

8586
<div class="prpl-buttons-wrapper">
86-
<button id="todo-list-completed-delete-all-cancel" onclick="prplTodoWidget.closeDeleteAllPopover()">
87+
<button id="todo-list-completed-delete-all-cancel">
8788
<?php
8889
/* translators: %1$s is the strong tag, %2$s is the closing strong tag. */
8990
\printf( \esc_html__( '%1$sNo%2$s, keep this list', 'progress-planner' ), '<strong>', '</strong>' );
9091
?>
9192
</button>
92-
<button id="todo-list-completed-delete-all-confirm" onclick="prplTodoWidget.deleteAllCompletedTasksAndClosePopover()">
93+
<button id="todo-list-completed-delete-all-confirm">
9394
<?php
9495
/* translators: %1$s is the strong tag, %2$s is the closing strong tag. */
9596
\printf( \esc_html__( '%1$sYes%2$s, delete all completed tasks', 'progress-planner' ), '<strong>', '</strong>' );

classes/class-suggested-tasks-db.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ public function get( $args = [] ) {
375375
)
376376
)
377377
);
378-
$results = array_merge( $results, $results_trashed );
378+
$results = \array_merge( $results, $results_trashed );
379379
}
380380

381381
\wp_cache_set( $cache_key, $results, static::GET_TASKS_CACHE_GROUP );

classes/class-suggested-tasks.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,6 @@ function ( $task ) use ( $args ) {
609609
* @return string
610610
*/
611611
public function get_task_id_from_slug( $slug ) {
612-
return explode( '__trashed', $slug )[0];
612+
return \explode( '__trashed', $slug )[0];
613613
}
614614
}

classes/suggested-tasks/providers/class-permalink-structure.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ public function handle_interactive_task_specific_submit() {
266266
\wp_send_json_error( [ 'message' => \esc_html__( 'Missing value.', 'progress-planner' ) ] );
267267
}
268268

269-
$permalink_structure = trim( \sanitize_text_field( \wp_unslash( $_POST['value'] ) ) );
269+
$permalink_structure = \trim( \sanitize_text_field( \wp_unslash( $_POST['value'] ) ) );
270270

271271
if ( empty( $permalink_structure ) ) {
272272
\wp_send_json_error( [ 'message' => \esc_html__( 'Invalid permalink structure.', 'progress-planner' ) ] );

classes/suggested-tasks/providers/class-remove-terms-without-posts.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,14 +343,14 @@ public function add_task_actions( $data = [], $actions = [] ) {
343343

344344
$actions[] = [
345345
'priority' => 10,
346-
'html' => sprintf(
346+
'html' => \sprintf(
347347
'<a href="#" class="prpl-tooltip-action-text prpl-delete-term-action" role="button"
348348
data-task-context=\'%s\'
349349
onclick="event.preventDefault(); document.getElementById(\'prpl-popover-%s\')?.showPopover(); this.dispatchEvent(new CustomEvent(\'prpl-interactive-task-action-remove-terms-without-posts\', { bubbles: true, detail: JSON.parse(this.dataset.taskContext) }));">
350350
%s
351351
</a>',
352-
htmlspecialchars(
353-
wp_json_encode(
352+
\htmlspecialchars(
353+
\wp_json_encode(
354354
[
355355
'post_title' => $task_details['post_title'],
356356
'target_term_id' => $task_data['target_term_id'],

0 commit comments

Comments
 (0)