diff --git a/README.md b/README.md index 92fd441..4814bfa 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ We don’t charge. We don’t sit in the middle. You use **your** own key. Every - **In the block editor** — Generate or regenerate alt text right from the Image block. - **On the media screen** — Button next to the alt field for single images. - **Logs & export** — See what ran, what cost what, and export as CSV. -- **Fixes older posts too** — Images already in posts pick up their new alt text automatically. +- **Fixes older posts too (optional)** — Turn it on and images already in posts pick up their new alt text automatically. - **Decorative images** — Purely decorative images (dividers, spacers, patterns) get empty alt text so screen readers skip them, as WCAG recommends. - **Spending limits** — See estimated spend per month and per model, and set a monthly cap. - **Your language** — Alt text and titles are written in your site's language (Polylang and WPML supported). @@ -150,9 +150,18 @@ The plugin uses the `everyalt` text domain and ships with a `.pot` in `languages ## 📝 Changelog ### Unreleased +- **Accessibility (WCAG 2.1 AA).** A full review of every screen, with all failures fixed: + - Review tabs: alt text and title fields now have labels; Save, Regenerate, and Edit identify which image they act on. + - Results (key validation, queue progress, saves, the Media screen button) are announced to screen readers via WordPress's `wp.a11y.speak`, once per batch rather than once per image. + - Keyboard focus stays on buttons while they work, instead of jumping to the top of the page. + - Success text and the "Key saved" badge now meet 4.5:1 contrast. + - Each tab has its own page title; the tab navigation is a `nav` landmark with the current tab marked. + - The Logs spending bar has an accessible name and value; scrollable log boxes are keyboard-reachable; links that open a new tab say so. +- **Changed** **Existing posts** (filling missing alt text in posts) is now **off by default**, so it's only applied when a site owner turns it on. +- **Improved** the default alt text prompt asks the model to include important visible text (logos, signs, headings). - **Fixed** estimated costs now include "thinking" tokens that some providers report outside the output count (Gemini's were missing, so Gemini costs were under-reported by about a third). Logs show them as **Thinking Tokens**, and the spending limit uses the corrected amounts. - **Improved** Gemini now uses Google's Interactions API, which sends images at low resolution (280 tokens instead of 1,120), cutting Gemini's cost per image by roughly half or more. Requests are sent with `store: false`, so Google doesn't keep them for later retrieval. -- **New: alt text shows up in existing posts.** WordPress copies an image's alt text into a post when the image is inserted, so alt text generated later never reached older posts. EveryAlt now fills in empty alt text in post content from the Media Library as pages are displayed (WordPress 6.0+). Posts aren't modified, and alt text written in a post is never replaced. On by default; turn off under **Settings → Existing posts**. +- **New: alt text shows up in existing posts.** WordPress copies an image's alt text into a post when the image is inserted, so alt text generated later never reached older posts. EveryAlt now fills in empty alt text in post content from the Media Library as pages are displayed (WordPress 6.0+). Posts aren't modified, and alt text written in a post is never replaced. Off by default; turn it on under **Settings → Existing posts**. - **New: background processing.** Bulk generation and new uploads now run in a background queue: close the tab and it keeps going, failures retry automatically (after 1, 5, and 30 minutes), and uploads finish immediately. The queue runs on WP-Cron and also whenever an EveryAlt page is open, so it works on sites where WP-Cron is blocked (e.g. behind HTTP auth). Bulk tabs gain a **Generate for all** button. Prefer the old behavior for uploads? Choose **Settings → New uploads → Generate during the upload**. - **New: decorative image detection.** When the AI judges an image clearly decorative, its alt text is left empty on purpose (per WCAG) and it's flagged on the Review Alt Text tab with a **Describe anyway** button. When in doubt, the AI describes the image. Turn off under **Settings → Decorative images**. - **New: spending controls.** The Logs tab shows estimated spend per month and per model. Set a **monthly spending limit** in Settings: when it's reached, generation pauses (queued images wait), admins see a notice, and it resumes next month or when the limit is raised. diff --git a/admin/class-everyalt-admin.php b/admin/class-everyalt-admin.php index 6c7b080..7825f28 100644 --- a/admin/class-everyalt-admin.php +++ b/admin/class-everyalt-admin.php @@ -409,7 +409,7 @@ public function enqueue_scripts() { wp_enqueue_script( $this->plugin_name, plugin_dir_url( __FILE__ ) . 'js/everyalt-admin-simple.js', - array( 'jquery' ), + array( 'jquery', 'wp-a11y' ), $this->every_alt_asset_version( 'js/everyalt-admin-simple.js' ), true ); @@ -429,6 +429,8 @@ public function enqueue_scripts() { 'queueNothing' => __( 'Those images were already queued.', 'everyalt' ), 'queueCleared' => __( 'Queue cleared.', 'everyalt' ), 'decorative' => __( 'Marked as decorative (alt text left empty on purpose)', 'everyalt' ), + /* translators: %d: number of images finished in this batch */ + 'queueBatchDone' => __( '%d finished.', 'everyalt' ), 'error' => __( 'Error', 'everyalt' ), 'errorPrefix' => __( 'Error:', 'everyalt' ), 'requestFailed' => __( 'Request failed', 'everyalt' ), @@ -508,7 +510,7 @@ public function every_alt_custom_button_to_media_edit_page() { wp_enqueue_script( $this->plugin_name . '-media', plugin_dir_url( __FILE__ ) . 'js/everyalt-media-simple.js', - array( 'jquery' ), + array( 'jquery', 'wp-a11y' ), $this->every_alt_asset_version( 'js/everyalt-media-simple.js' ), true ); @@ -1065,14 +1067,56 @@ public function add_options_page() { + /** + * The admin page's tabs, slug => label. + * + * @return array + */ + public static function tabs() { + return array( + 'settings' => __( 'Settings', 'everyalt' ), + 'bulk' => __( 'Bulk Alt Text Generator', 'everyalt' ), + 'review' => __( 'Review Alt Text', 'everyalt' ), + 'bulk_title' => __( 'Bulk Image Title Generator', 'everyalt' ), + 'review_title' => __( 'Review Image Titles', 'everyalt' ), + 'logs' => __( 'Logs', 'everyalt' ), + ); + } + + /** + * Tab being viewed on the EveryAlt page (defaults to settings). + * + * @return string + */ + public static function current_tab() { + $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; + return array_key_exists( $tab, self::tabs() ) ? $tab : 'settings'; + } + + /** + * Filter: admin_title. Put the tab name in the browser title, so each tab has a distinct page + * title (WCAG 2.4.2), e.g. "Logs ‹ EveryAlt ‹ My Site — WordPress". + * + * @param string $admin_title + * @param string $title + * @return string + */ + public function every_alt_admin_title( $admin_title, $title ) { + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + if ( $page !== $this->plugin_name ) { + return $admin_title; + } + $tabs = self::tabs(); + return $tabs[ self::current_tab() ] . ' ‹ ' . $admin_title; + } + /** * Render the options page for plugin * * @since 1.0.0 */ public function display_options_page() { - $allowed_tabs = array( 'settings', 'bulk', 'review', 'bulk_title', 'review_title', 'logs' ); - $tab = isset( $_GET['tab'] ) && in_array( $_GET['tab'], $allowed_tabs, true ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; + $tab = self::current_tab(); $active = $tab; diff --git a/admin/class-everyalt-openai.php b/admin/class-everyalt-openai.php index 5d33f71..4d062e3 100644 --- a/admin/class-everyalt-openai.php +++ b/admin/class-everyalt-openai.php @@ -13,7 +13,7 @@ class Every_Alt_OpenAI { - const DEFAULT_PROMPT = 'Describe this image in one short, clear sentence suitable for HTML alt text. Do not start with "This image shows" or similar. Output only the alt text, nothing else.'; + const DEFAULT_PROMPT = 'Describe this image in one short, clear sentence suitable for HTML alt text. If the image contains important text (such as a logo, sign, or heading), include that text. Do not start with "This image shows" or similar. Output only the alt text, nothing else.'; const DEFAULT_TITLE_PROMPT = 'Write a short, descriptive title for this image, suitable for a WordPress image title (about 3 to 6 words, Title Case). Do not use quotation marks, a trailing period, or phrases like "This image shows". Output only the title, nothing else.'; diff --git a/admin/css/everyalt-admin.css b/admin/css/everyalt-admin.css index 1a2358e..5614719 100644 --- a/admin/css/everyalt-admin.css +++ b/admin/css/everyalt-admin.css @@ -1 +1 @@ -*{box-sizing:inherit}.everyalt-settings-page{box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;font-size:14px;position:relative}#everyalt-history-images-empty{margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:0 15px}#everyalt-history-images{grid-column-gap:10px;grid-row-gap:10px;display:grid;grid-template-columns:repeat(2,1fr);margin-top:2rem;padding-left:15px;padding-right:15px}@media (min-width:768px){#everyalt-history-images{grid-template-columns:repeat(3,1fr)}}@media (min-width:1200px){#everyalt-history-images{grid-template-columns:repeat(4,1fr)}}@media (min-width:1400px){#everyalt-history-images{grid-template-columns:repeat(5,1fr)}}@media (min-width:1800px){#everyalt-history-images{grid-template-columns:repeat(7,1fr)}}#everyalt-history-images img{height:auto;width:100%}#everyalt-history-images a{display:block;margin-bottom:1rem}#everyalt-history .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-custom-media-button .components-snackbar-list{bottom:15px;position:fixed;right:15px}#everyalt-custom-media-button .components-snackbar-list__notice-container{display:flex;justify-content:end}#everyalt-bulk-generator{margin-left:auto;margin-right:auto;max-width:1280px;padding:0 15px}#everyalt-bulk-generator .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar{margin-top:1rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-bulk-generator .components-card .components-button.is-primary{background-color:#0750b6;text-transform:uppercase}#everyalt-bulk-generator table{margin-top:2rem;width:100%}#everyalt-bulk-generator table th{background-color:#00205b;color:#fff;font-size:.75rem;padding:5px;text-align:left;text-transform:uppercase;width:50%}#everyalt-bulk-generator table td{font-size:.75rem;padding:5px}#everyalt-bulk-generator table td.has_error{background-color:crimson;color:#fff}#everyalt-bulk-generator table tr.even{background-color:#e8e8e8}#everyalt-bulk-generator .everyalt-spinner{display:flex;justify-content:center;text-align:center}#everyalt-bulk-generator .everyalt-spinner-container{text-align:center}#everyalt-bulk-generator .completed-message{padding:20px;text-align:center}#everyalt-bulk-generator .completed-message h2{color:#00205b}#everyalt-plugin .components-snackbar-list{bottom:15px;left:0;position:fixed;width:100%}#everyalt-plugin .is-primary{background-color:#0750b6}#everyalt-plugin .every_alt_http_password_holder{margin-top:50px}#everyalt-plugin .every_alt_http_password_holder .components-base-control__help{margin-top:-15px}#everyalt-plugin .every_alt_http_password_holder h2{margin-bottom:5px}#everyalt-plugin .everyalt-main{padding:0 15px}#everyalt-plugin .components-form-toggle.is-checked .components-form-toggle__track{background-color:#0750b6}#everyalt-plugin .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar{margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-plugin .components-card .components-toggle-control{margin-top:2rem}#everyalt-plugin .components-card .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{background-color:#0750b6;margin-right:10px}#everyalt-plugin .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-plugin .components-notice{margin:0;width:100%}#everyalt-plugin .components-placeholder{background:#f6f6f6;box-shadow:none}#everyalt-plugin .components-placeholder .components-placeholder__fieldset{display:flex;justify-content:center}#everyalt-plugin .everyalt-main-error{margin-top:2rem}#everyalt-plugin .everyalt-main{margin-left:auto;margin-right:auto;max-width:1280px}#everyalt-plugin .everyalt-main .components-panel__body{background:#fff;border:1px solid #e2e4e7;margin:25px 0}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__body-toggle{border-bottom:1px solid #d6e2ed}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row{margin:0;padding:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{margin-right:10px}#everyalt-plugin .everyalt-main .everyalt-info{padding:0 10px 10px}#everyalt-plugin .everyalt-main .everyalt-info h2{font-weight:300;margin-bottom:10px}#everyalt-plugin .everyalt-main .everyalt-info p{margin:0 0 1.5em}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group{display:flex}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default{font-size:14px;height:auto;padding:2px 20px}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default:first-child{margin-right:15px}#every_alt_pagination{display:flex;font-size:.85rem;font-weight:700;justify-content:center;margin-top:2rem}#every_alt_pagination a.page-numbers{background-color:#0750b6;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center;text-decoration:none}#every_alt_pagination a.page-numbers:hover{background-color:#00205b}#every_alt_pagination a.next,#every_alt_pagination a.prev{background-color:transparent;color:#0750b6;padding:0}#every_alt_pagination a.next:hover,#every_alt_pagination a.prev:hover{background-color:transparent;color:#00205b}#every_alt_pagination a.next{margin-left:10px}#every_alt_pagination a.prev{margin-right:10px}#every_alt_pagination span.current{background-color:#00205b;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center}#everyalt-main-error{background-color:#fefefa;margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:15px}#everyalt-main-error h2{color:#00205b;text-align:center!important}#everyalt-gutenber-button p{font-size:.75rem}.everyalt-progress.hidden,.hidden{display:none!important}.everyalt-settings-wrap .form-table,.everyalt-bulk-wrap table,.everyalt-history-wrap table{margin-top:1em}.everyalt-log-detail{max-width:480px;vertical-align:top}.everyalt-log-detail-inner{max-height:280px;overflow-y:auto;overflow-x:auto;padding:6px 8px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px;font-size:12px;line-height:1.4;white-space:pre-wrap;word-break:break-word}.everyalt-key-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-key-row .regular-text{flex:1;min-width:200px}.everyalt-bulk-actions{margin:1em 0}.everyalt-bulk-actions .button{margin-right:8px}.everyalt-bulk-progress{margin:1em 0;padding:1em;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px}.everyalt-bulk-progress-bar{height:22px;background:#fff;border:1px solid #c3c4c7;border-radius:3px;overflow:hidden}.everyalt-bulk-progress-bar span{display:block;height:100%;background:#2271b1;transition:width .2s}.everyalt-bulk-progress-log{margin:.5em 0 0;padding-left:1.5em;max-height:200px;overflow-y:auto;font-size:13px}.everyalt-bulk-progress-log li.success{color:#00a32a}.everyalt-bulk-progress-log li.error{color:#b32d2e}.everyalt-bulk-item-status.success{color:#00a32a}.everyalt-bulk-item-status.error{color:#b32d2e}.everyalt-review-wrap .description{margin-bottom:1em}.everyalt-review-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-review-item{border:1px solid #c3c4c7;border-radius:4px;padding:10px;background:#fff}.everyalt-review-item .everyalt-review-thumb{display:block;line-height:0;margin-bottom:8px}.everyalt-review-item .everyalt-review-thumb img{width:100%;height:auto;display:block}.everyalt-review-item .everyalt-review-edit-link{display:inline-block;font-size:12px;margin-bottom:6px}.everyalt-review-item .everyalt-review-alt-field{width:100%;margin:0 0 8px;padding:6px;font-size:13px;min-height:60px;display:block;box-sizing:border-box}.everyalt-review-item .everyalt-review-actions{display:flex;gap:6px;flex-wrap:wrap}.everyalt-review-item .everyalt-review-actions .button{margin:0}.everyalt-review-item .everyalt-review-status{display:block;margin-top:6px;font-size:12px}.everyalt-review-item .everyalt-review-status.success{color:#00a32a}.everyalt-review-item .everyalt-review-status.error{color:#b32d2e}.everyalt-page-count{margin:1em 0 0;color:#646970}.everyalt-pagination{display:flex;flex-wrap:wrap;gap:4px;margin:1.5em 0}.everyalt-pagination .page-numbers{display:inline-block;min-width:30px;padding:4px 8px;text-align:center;border:1px solid #c3c4c7;border-radius:3px;background:#fff;text-decoration:none}.everyalt-pagination .page-numbers.current{background:#2271b1;border-color:#2271b1;color:#fff}.everyalt-pagination .page-numbers.dots{border-color:transparent;background:transparent}.everyalt-model-list{display:grid;gap:8px;max-width:640px}.form-table td fieldset.everyalt-model-list label.everyalt-model-option{display:grid;grid-template-columns:auto 1fr;column-gap:8px;align-items:baseline;padding:10px 12px;border:1px solid #c3c4c7;border-radius:4px;background:#fff;cursor:pointer}.form-table td fieldset.everyalt-model-list label.everyalt-model-option input{grid-row:span 3;margin:2px 0 0}.everyalt-model-option .everyalt-model-provider,.everyalt-model-option .everyalt-model-price{display:block}.everyalt-model-option:has(input:checked){border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}.everyalt-model-provider,.everyalt-model-price{color:#646970;font-size:13px}.everyalt-provider-key{max-width:640px;margin-bottom:1.5em}.everyalt-provider-key-title{display:flex;align-items:center;gap:10px;margin:0 0 8px;font-size:14px}.everyalt-key-status{font-size:12px;font-weight:400;padding:1px 8px;border-radius:10px}.everyalt-key-status.is-saved{background:#edfaef;color:#008a20}.everyalt-key-status.is-missing{background:#fcf9e8;color:#8a6d00}.everyalt-key-steps{margin:10px 0 8px 1.5em}.everyalt-key-steps li{margin-bottom:4px}.everyalt-privacy-note{margin-top:10px;padding:10px 12px;border-left:4px solid #00a32a;background:#f6f7f7}.everyalt-privacy-note p{margin:0 0 6px}.everyalt-privacy-note p:last-child{margin-bottom:0}.everyalt-queue-panel p{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-queue-panel .everyalt-queue-spinner{float:none;margin:0}.everyalt-queue-panel:not(.is-working) .everyalt-queue-spinner{display:none}.everyalt-queue-panel .everyalt-bulk-progress-log{margin:0 0 8px;max-height:160px}.everyalt-queue-panel .everyalt-bulk-progress-log:empty{display:none}#everyalt-queue-clear{margin-left:auto}.everyalt-review-item.is-decorative{border-style:dashed}.everyalt-decorative-badge{display:inline-block;margin:0 6px 6px 0;padding:1px 8px;border-radius:10px;background:#f0f6fc;color:#0a4b78;font-size:12px}.everyalt-budget-bar{max-width:480px;margin:0 0 1em}.everyalt-budget-bar.is-full span{background:#d63638}.everyalt-usage-table{max-width:900px;margin-bottom:.5em}/* Bulk tabs: selectable image cards (alt text and titles) */.everyalt-bulk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-bulk-card{position:relative;margin:0;padding:6px;background:#fff;border:1px solid #dcdcde;border-radius:4px;transition:border-color .15s,box-shadow .15s}.everyalt-bulk-card:hover{border-color:#8c8f94}.everyalt-bulk-card.is-selected{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}.everyalt-bulk-card.is-error{border-color:#d63638}.everyalt-bulk-card label{position:relative;display:block;cursor:pointer}.everyalt-bulk-card .everyalt-bulk-thumb{position:relative;display:block;line-height:0;border-radius:2px;overflow:hidden;background:#f0f0f1}.everyalt-bulk-card .everyalt-bulk-thumb img{display:block;width:100%;height:auto;aspect-ratio:1/1;object-fit:cover;transition:opacity .2s}.everyalt-bulk-card label input[type=checkbox]{position:absolute;top:8px;left:8px;z-index:2;margin:0;box-shadow:0 0 0 3px rgba(255,255,255,.9),0 1px 4px 3px rgba(0,0,0,.25)}.everyalt-bulk-card.is-done label{cursor:default}.everyalt-bulk-card.is-done .everyalt-bulk-thumb img{opacity:.4}.everyalt-bulk-card.is-done .everyalt-bulk-thumb::after{content:"\2713";position:absolute;top:50%;left:50%;width:40px;height:40px;margin:-20px 0 0 -20px;border-radius:50%;background:#00a32a;color:#fff;font-size:22px;font-weight:600;line-height:40px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.3)}.everyalt-bulk-card .everyalt-bulk-edit-link{display:inline-block;margin-top:6px;font-size:12px}.everyalt-bulk-item-status{display:block;margin-top:4px;font-size:12px;line-height:1.35;word-break:break-word}.everyalt-bulk-item-status:empty{display:none} \ No newline at end of file +*{box-sizing:inherit}.everyalt-settings-page{box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;font-size:14px;position:relative}#everyalt-history-images-empty{margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:0 15px}#everyalt-history-images{grid-column-gap:10px;grid-row-gap:10px;display:grid;grid-template-columns:repeat(2,1fr);margin-top:2rem;padding-left:15px;padding-right:15px}@media (min-width:768px){#everyalt-history-images{grid-template-columns:repeat(3,1fr)}}@media (min-width:1200px){#everyalt-history-images{grid-template-columns:repeat(4,1fr)}}@media (min-width:1400px){#everyalt-history-images{grid-template-columns:repeat(5,1fr)}}@media (min-width:1800px){#everyalt-history-images{grid-template-columns:repeat(7,1fr)}}#everyalt-history-images img{height:auto;width:100%}#everyalt-history-images a{display:block;margin-bottom:1rem}#everyalt-history .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-custom-media-button .components-snackbar-list{bottom:15px;position:fixed;right:15px}#everyalt-custom-media-button .components-snackbar-list__notice-container{display:flex;justify-content:end}#everyalt-bulk-generator{margin-left:auto;margin-right:auto;max-width:1280px;padding:0 15px}#everyalt-bulk-generator .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar{margin-top:1rem}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-bulk-generator .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-bulk-generator .components-card .components-button.is-primary{background-color:#0750b6;text-transform:uppercase}#everyalt-bulk-generator table{margin-top:2rem;width:100%}#everyalt-bulk-generator table th{background-color:#00205b;color:#fff;font-size:.75rem;padding:5px;text-align:left;text-transform:uppercase;width:50%}#everyalt-bulk-generator table td{font-size:.75rem;padding:5px}#everyalt-bulk-generator table td.has_error{background-color:crimson;color:#fff}#everyalt-bulk-generator table tr.even{background-color:#e8e8e8}#everyalt-bulk-generator .everyalt-spinner{display:flex;justify-content:center;text-align:center}#everyalt-bulk-generator .everyalt-spinner-container{text-align:center}#everyalt-bulk-generator .completed-message{padding:20px;text-align:center}#everyalt-bulk-generator .completed-message h2{color:#00205b}#everyalt-plugin .components-snackbar-list{bottom:15px;left:0;position:fixed;width:100%}#everyalt-plugin .is-primary{background-color:#0750b6}#everyalt-plugin .every_alt_http_password_holder{margin-top:50px}#everyalt-plugin .every_alt_http_password_holder .components-base-control__help{margin-top:-15px}#everyalt-plugin .every_alt_http_password_holder h2{margin-bottom:5px}#everyalt-plugin .everyalt-main{padding:0 15px}#everyalt-plugin .components-form-toggle.is-checked .components-form-toggle__track{background-color:#0750b6}#everyalt-plugin .components-card{background-color:#fefefa;margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar{margin-top:2rem}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-wrapper{background-color:#fff;border:1px solid #b7c9d9;border-radius:3px;height:20px;overflow:hidden;position:relative}#everyalt-plugin .components-card .everyalt-progress-bar .every-bar-bar{background-color:#0750b6;height:100%;left:0;position:absolute;top:0}#everyalt-plugin .components-card .components-toggle-control{margin-top:2rem}#everyalt-plugin .components-card .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .components-card .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{background-color:#0750b6;margin-right:10px}#everyalt-plugin .components-snackbar-list__notice-container{display:flex;justify-content:end;padding-right:15px}#everyalt-plugin .components-notice{margin:0;width:100%}#everyalt-plugin .components-placeholder{background:#f6f6f6;box-shadow:none}#everyalt-plugin .components-placeholder .components-placeholder__fieldset{display:flex;justify-content:center}#everyalt-plugin .everyalt-main-error{margin-top:2rem}#everyalt-plugin .everyalt-main{margin-left:auto;margin-right:auto;max-width:1280px}#everyalt-plugin .everyalt-main .components-panel__body{background:#fff;border:1px solid #e2e4e7;margin:25px 0}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__body-toggle{border-bottom:1px solid #d6e2ed}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row{margin:0;padding:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control .components-base-control__field{align-items:center;display:flex;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field{width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field{display:block}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field label{font-weight:600;margin-bottom:10px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input{border:1px solid #b7c9d9;border-radius:3px;box-shadow:none;color:#444;font-size:14px;height:40px;margin:0 0 10px;padding:9px 15px;width:100%}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:disabled{background:#f6f6f6;border-color:#eee;color:#bbb}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field input:focus{border-color:#00a0d2;box-shadow:0 0 0 1px #00a0d2;outline:2px solid transparent;outline-offset:-2px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group{align-items:center;display:flex;margin:2px 0 15px}#everyalt-plugin .everyalt-main .components-panel__body .components-panel__row .components-base-control.everyalt-text-field .components-base-control__field .everyalt-text-field-button-group .is-primary{margin-right:10px}#everyalt-plugin .everyalt-main .everyalt-info{padding:0 10px 10px}#everyalt-plugin .everyalt-main .everyalt-info h2{font-weight:300;margin-bottom:10px}#everyalt-plugin .everyalt-main .everyalt-info p{margin:0 0 1.5em}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group{display:flex}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default{font-size:14px;height:auto;padding:2px 20px}#everyalt-plugin .everyalt-main .everyalt-info .everyalt-info-button-group .is-default:first-child{margin-right:15px}#every_alt_pagination{display:flex;font-size:.85rem;font-weight:700;justify-content:center;margin-top:2rem}#every_alt_pagination a.page-numbers{background-color:#0750b6;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center;text-decoration:none}#every_alt_pagination a.page-numbers:hover{background-color:#00205b}#every_alt_pagination a.next,#every_alt_pagination a.prev{background-color:transparent;color:#0750b6;padding:0}#every_alt_pagination a.next:hover,#every_alt_pagination a.prev:hover{background-color:transparent;color:#00205b}#every_alt_pagination a.next{margin-left:10px}#every_alt_pagination a.prev{margin-right:10px}#every_alt_pagination span.current{background-color:#00205b;color:#fff;display:inline-block;margin-right:-4px;min-width:20px;padding:8px;text-align:center}#everyalt-main-error{background-color:#fefefa;margin-left:auto;margin-right:auto;margin-top:2rem;max-width:1280px;padding:15px}#everyalt-main-error h2{color:#00205b;text-align:center!important}#everyalt-gutenber-button p{font-size:.75rem}.everyalt-progress.hidden,.hidden{display:none!important}.everyalt-settings-wrap .form-table,.everyalt-bulk-wrap table,.everyalt-history-wrap table{margin-top:1em}.everyalt-log-detail{max-width:480px;vertical-align:top}.everyalt-log-detail-inner{max-height:280px;overflow-y:auto;overflow-x:auto;padding:6px 8px;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px;font-size:12px;line-height:1.4;white-space:pre-wrap;word-break:break-word}.everyalt-key-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-key-row .regular-text{flex:1;min-width:200px}.everyalt-bulk-actions{margin:1em 0}.everyalt-bulk-actions .button{margin-right:8px}.everyalt-bulk-progress{margin:1em 0;padding:1em;background:#f6f7f7;border:1px solid #c3c4c7;border-radius:3px}.everyalt-bulk-progress-bar{height:22px;background:#fff;border:1px solid #c3c4c7;border-radius:3px;overflow:hidden}.everyalt-bulk-progress-bar span{display:block;height:100%;background:#2271b1;transition:width .2s}.everyalt-bulk-progress-log{margin:.5em 0 0;padding-left:1.5em;max-height:200px;overflow-y:auto;font-size:13px}.everyalt-bulk-progress-log li.success{color:#00701a}.everyalt-bulk-progress-log li.error{color:#b32d2e}.everyalt-bulk-item-status.success{color:#00701a}.everyalt-bulk-item-status.error{color:#b32d2e}.everyalt-review-wrap .description{margin-bottom:1em}.everyalt-review-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-review-item{border:1px solid #c3c4c7;border-radius:4px;padding:10px;background:#fff}.everyalt-review-item .everyalt-review-thumb{display:block;line-height:0;margin-bottom:8px}.everyalt-review-item .everyalt-review-thumb img{width:100%;height:auto;display:block}.everyalt-review-item .everyalt-review-edit-link{display:inline-block;font-size:12px;margin-bottom:6px}.everyalt-review-item .everyalt-review-alt-field{width:100%;margin:0 0 8px;padding:6px;font-size:13px;min-height:60px;display:block;box-sizing:border-box}.everyalt-review-item .everyalt-review-actions{display:flex;gap:6px;flex-wrap:wrap}.everyalt-review-item .everyalt-review-actions .button{margin:0}.everyalt-review-item .everyalt-review-status{display:block;margin-top:6px;font-size:12px}.everyalt-review-item .everyalt-review-status.success{color:#00701a}.everyalt-review-item .everyalt-review-status.error{color:#b32d2e}.everyalt-page-count{margin:1em 0 0;color:#646970}.everyalt-pagination{display:flex;flex-wrap:wrap;gap:4px;margin:1.5em 0}.everyalt-pagination .page-numbers{display:inline-block;min-width:30px;padding:4px 8px;text-align:center;border:1px solid #c3c4c7;border-radius:3px;background:#fff;text-decoration:none}.everyalt-pagination .page-numbers.current{background:#2271b1;border-color:#2271b1;color:#fff}.everyalt-pagination .page-numbers.dots{border-color:transparent;background:transparent}.everyalt-model-list{display:grid;gap:8px;max-width:640px}.form-table td fieldset.everyalt-model-list label.everyalt-model-option{display:grid;grid-template-columns:auto 1fr;column-gap:8px;align-items:baseline;padding:10px 12px;border:1px solid #c3c4c7;border-radius:4px;background:#fff;cursor:pointer}.form-table td fieldset.everyalt-model-list label.everyalt-model-option input{grid-row:span 3;margin:2px 0 0}.everyalt-model-option .everyalt-model-provider,.everyalt-model-option .everyalt-model-price{display:block}.everyalt-model-option:has(input:checked){border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}.everyalt-model-provider,.everyalt-model-price{color:#646970;font-size:13px}.everyalt-provider-key{max-width:640px;margin-bottom:1.5em}.everyalt-provider-key-title{display:flex;align-items:center;gap:10px;margin:0 0 8px;font-size:14px}.everyalt-key-status{font-size:12px;font-weight:400;padding:1px 8px;border-radius:10px}.everyalt-key-status.is-saved{background:#edfaef;color:#00701a}.everyalt-key-status.is-missing{background:#fcf9e8;color:#8a6d00}.everyalt-key-steps{margin:10px 0 8px 1.5em}.everyalt-key-steps li{margin-bottom:4px}.everyalt-privacy-note{margin-top:10px;padding:10px 12px;border-left:4px solid #00a32a;background:#f6f7f7}.everyalt-privacy-note p{margin:0 0 6px}.everyalt-privacy-note p:last-child{margin-bottom:0}.everyalt-queue-panel p{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.everyalt-queue-panel .everyalt-queue-spinner{float:none;margin:0}.everyalt-queue-panel:not(.is-working) .everyalt-queue-spinner{display:none}.everyalt-queue-panel .everyalt-bulk-progress-log{margin:0 0 8px;max-height:160px}.everyalt-queue-panel .everyalt-bulk-progress-log:empty{display:none}#everyalt-queue-clear{margin-left:auto}.everyalt-review-item.is-decorative{border-style:dashed}.everyalt-decorative-badge{display:inline-block;margin:0 6px 6px 0;padding:1px 8px;border-radius:10px;background:#f0f6fc;color:#0a4b78;font-size:12px}.everyalt-budget-bar{max-width:480px;margin:0 0 1em}.everyalt-budget-bar.is-full span{background:#d63638}.everyalt-usage-table{max-width:900px;margin-bottom:.5em}/* Bulk tabs: selectable image cards (alt text and titles) */.everyalt-bulk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;list-style:none;margin:1em 0;padding:0}.everyalt-bulk-card{position:relative;margin:0;padding:6px;background:#fff;border:1px solid #dcdcde;border-radius:4px;transition:border-color .15s,box-shadow .15s}.everyalt-bulk-card:hover{border-color:#8c8f94}.everyalt-bulk-card.is-selected{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1}.everyalt-bulk-card.is-error{border-color:#d63638}.everyalt-bulk-card label{position:relative;display:block;cursor:pointer}.everyalt-bulk-card .everyalt-bulk-thumb{position:relative;display:block;line-height:0;border-radius:2px;overflow:hidden;background:#f0f0f1}.everyalt-bulk-card .everyalt-bulk-thumb img{display:block;width:100%;height:auto;aspect-ratio:1/1;object-fit:cover;transition:opacity .2s}.everyalt-bulk-card label input[type=checkbox]{position:absolute;top:8px;left:8px;z-index:2;margin:0;box-shadow:0 0 0 3px rgba(255,255,255,.9),0 1px 4px 3px rgba(0,0,0,.25)}.everyalt-bulk-card.is-done label{cursor:default}.everyalt-bulk-card.is-done .everyalt-bulk-thumb img{opacity:.4}.everyalt-bulk-card.is-done .everyalt-bulk-thumb::after{content:"\2713";position:absolute;top:50%;left:50%;width:40px;height:40px;margin:-20px 0 0 -20px;border-radius:50%;background:#00a32a;color:#fff;font-size:22px;font-weight:600;line-height:40px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.3)}.everyalt-bulk-card .everyalt-bulk-edit-link{display:inline-block;margin-top:6px;font-size:12px}.everyalt-bulk-item-status{display:block;margin-top:4px;font-size:12px;line-height:1.35;word-break:break-word}.everyalt-bulk-item-status:empty{display:none}/* Busy buttons use aria-disabled (not disabled) so keyboard focus stays put; style them like disabled ones. */.wp-core-ui .button[aria-disabled="true"],.wp-core-ui .button-primary[aria-disabled="true"]{color:#a7aaad!important;border-color:#dcdcde!important;background:#f6f7f7!important;box-shadow:none!important;cursor:default} \ No newline at end of file diff --git a/admin/js/everyalt-admin-simple.js b/admin/js/everyalt-admin-simple.js index a27a379..5dff12e 100644 --- a/admin/js/everyalt-admin-simple.js +++ b/admin/js/everyalt-admin-simple.js @@ -11,6 +11,21 @@ var i18n = (typeof everyaltAdmin !== 'undefined' && everyaltAdmin.i18n) || {}; function t(key, fallback) { return i18n[key] || fallback; } + // Announce a message to screen readers via WordPress's shared live regions (wp.a11y.speak), which + // are always present in the page, so the first message isn't lost like it can be with a live + // region that was just revealed. + function speak(message, assertive) { + if (message && window.wp && wp.a11y && wp.a11y.speak) wp.a11y.speak(message, assertive ? 'assertive' : 'polite'); + } + + // Mark a button as busy without the disabled attribute: a disabled button loses keyboard focus + // (it jumps to the top of the page), while aria-disabled keeps the user's place. Clicks are + // ignored while busy. + function isBusy(btn) { return btn.getAttribute('aria-disabled') === 'true'; } + function setBusy(btn, busy) { + if (busy) btn.setAttribute('aria-disabled', 'true'); else btn.removeAttribute('aria-disabled'); + } + function request(method, path, body) { var url = restUrl.replace(/\/$/, '') + path; var opts = { @@ -55,6 +70,7 @@ var p = document.createElement('p'); p.textContent = message; resultEl.appendChild(p); + speak(message, !ok); } validateKeyBtn.addEventListener('click', function() { var keyInput = block ? block.querySelector('input[type="password"]') : null; @@ -62,7 +78,8 @@ if (!resultEl) return; resultEl.style.display = 'none'; resultEl.className = 'everyalt-validate-result'; - validateKeyBtn.disabled = true; + if (isBusy(validateKeyBtn)) return; + setBusy(validateKeyBtn, true); var formData = new FormData(); formData.append('action', 'everyalt_validate_key'); formData.append('nonce', validateKeyNonce); @@ -75,12 +92,12 @@ }) .then(function(r) { return r.json(); }) .then(function(data) { - validateKeyBtn.disabled = false; + setBusy(validateKeyBtn, false); var msg = (data.data && data.data.message) ? data.data.message : (data.success ? '' : t('error', 'Error')); showResult(!!data.success, msg); }) .catch(function() { - validateKeyBtn.disabled = false; + setBusy(validateKeyBtn, false); showResult(false, t('requestFailed', 'Request failed')); }); }); @@ -182,8 +199,13 @@ (function step() { request('POST', '/everyalt-api/v1/queue/process') .then(function(data) { - (data.results || []).forEach(markItem); + var results = data.results || []; + results.forEach(markItem); renderQueue(data.status); + // One summary per batch rather than one announcement per image. + if (results.length || (data.status && (data.status.paused || !data.status.total))) { + speak((results.length ? fmt(t('queueBatchDone', '%d finished.'), results.length) + ' ' : '') + queueText.textContent, !!(data.status && data.status.paused)); + } var st = data.status || {}; if (st.paused || !st.total) { queueRunning = false; return; } if (data.locked || !st.due) { @@ -200,22 +222,27 @@ } function addToQueue(body, btn) { - if (btn) btn.disabled = true; + if (btn) { + if (isBusy(btn)) return Promise.resolve(); + setBusy(btn, true); + } return request('POST', '/everyalt-api/v1/queue', body) .then(function(data) { - if (btn) btn.disabled = false; + if (btn) setBusy(btn, false); if (queueLog) { var li = document.createElement('li'); li.textContent = data.added ? fmt(t('queueAdded', 'Added %d images to the queue.'), data.added) : t('queueNothing', 'Those images were already queued.'); queueLog.insertBefore(li, queueLog.firstChild); + speak(li.textContent); } renderQueue(data.status); runQueue(); }) .catch(function(err) { - if (btn) btn.disabled = false; + if (btn) setBusy(btn, false); renderQueue({ total: 0 }); queueText.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); + speak(queueText.textContent, true); }); } @@ -239,6 +266,7 @@ request('DELETE', '/everyalt-api/v1/queue').then(function(status) { renderQueue(status); queueText.textContent = t('queueCleared', 'Queue cleared.'); + speak(queueText.textContent); }); }); } @@ -257,23 +285,26 @@ var textarea = item ? item.querySelector('.everyalt-review-alt-field') : null; var statusEl = item ? item.querySelector('.everyalt-review-status') : null; var altText = textarea ? textarea.value : ''; - btn.disabled = true; + if (isBusy(btn)) return; + setBusy(btn, true); request('POST', '/everyalt-api/v1/save_alt', { media_id: mediaId, alt_text: altText }) .then(function() { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('saved', 'Saved!'); + speak(statusEl.textContent); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } }) .catch(function(err) { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } }); @@ -287,31 +318,35 @@ var item = btn.closest('.everyalt-review-item'); var textarea = item ? item.querySelector('.everyalt-review-alt-field') : null; var statusEl = item ? item.querySelector('.everyalt-review-status') : null; - btn.disabled = true; + if (isBusy(btn)) return; + setBusy(btn, true); if (statusEl) statusEl.textContent = ''; var body = { media_id: mediaId }; if (btn.getAttribute('data-describe')) body.describe = true; request('POST', '/everyalt-api/v1/bulk_generate_alt', body) .then(function(data) { - btn.disabled = false; + setBusy(btn, false); if (data && data.success && data.alt_text !== undefined) { if (textarea) textarea.value = data.alt_text; if (statusEl) { statusEl.textContent = data.decorative ? t('decorative', 'Marked as decorative') : t('regenerated', 'Regenerated!'); + speak(statusEl.textContent); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } } }) .catch(function(err) { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } }); @@ -342,23 +377,26 @@ var textarea = item ? item.querySelector('.everyalt-review-title-field') : null; var statusEl = item ? item.querySelector('.everyalt-review-status') : null; var title = textarea ? textarea.value : ''; - btn.disabled = true; + if (isBusy(btn)) return; + setBusy(btn, true); request('POST', '/everyalt-api/v1/save_title', { media_id: mediaId, title: title }) .then(function() { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('saved', 'Saved!'); + speak(statusEl.textContent); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } }) .catch(function(err) { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('saveFailed', 'Save failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } }); @@ -372,29 +410,33 @@ var item = btn.closest('.everyalt-review-title-item'); var textarea = item ? item.querySelector('.everyalt-review-title-field') : null; var statusEl = item ? item.querySelector('.everyalt-review-status') : null; - btn.disabled = true; + if (isBusy(btn)) return; + setBusy(btn, true); if (statusEl) statusEl.textContent = ''; request('POST', '/everyalt-api/v1/bulk_generate_title', { media_id: mediaId }) .then(function(data) { - btn.disabled = false; + setBusy(btn, false); if (data && data.success && data.title !== undefined) { if (textarea) textarea.value = data.title; if (statusEl) { statusEl.textContent = t('regenerated', 'Regenerated!'); + speak(statusEl.textContent); statusEl.className = 'everyalt-review-status success'; setTimeout(function() { statusEl.textContent = ''; statusEl.className = 'everyalt-review-status'; }, 2000); } } else { if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (data && data.message ? data.message : t('regenerateFailed', 'Regenerate failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } } }) .catch(function(err) { - btn.disabled = false; + setBusy(btn, false); if (statusEl) { statusEl.textContent = t('errorPrefix', 'Error:') + ' ' + (err && err.message ? err.message : t('requestFailed', 'Request failed')); + speak(statusEl.textContent, true); statusEl.className = 'everyalt-review-status error'; } }); diff --git a/admin/js/everyalt-gutenberg-button.js b/admin/js/everyalt-gutenberg-button.js index f1ab429..583e70a 100644 --- a/admin/js/everyalt-gutenberg-button.js +++ b/admin/js/everyalt-gutenberg-button.js @@ -79,6 +79,9 @@ onClick: generateAlt, isBusy: isBusy, disabled: isBusy, + // Keep the button focusable while busy (aria-disabled), so keyboard focus isn't lost. + accessibleWhenDisabled: true, + __experimentalIsFocusable: true, style: { marginTop: '8px' } }, t('button', 'Generate alt text with EveryAlt')) : el('p', { className: 'everyalt-gutenberg-help', style: { margin: 0, fontSize: '12px', color: '#757575' } }, diff --git a/admin/js/everyalt-media-simple.js b/admin/js/everyalt-media-simple.js index 85a84c2..e0b83b7 100644 --- a/admin/js/everyalt-media-simple.js +++ b/admin/js/everyalt-media-simple.js @@ -10,6 +10,10 @@ var mediaId = config.mediaId || 0; var i18n = config.i18n || {}; function t(key, fallback) { return i18n[key] || fallback; } + // Announce results to screen readers (the message paragraph alone is not a live region). + function speak(message, assertive) { + if (message && window.wp && wp.a11y && wp.a11y.speak) wp.a11y.speak(message, assertive ? 'assertive' : 'polite'); + } if (!restUrl || !mediaId) return; @@ -28,8 +32,11 @@ wrap.appendChild(msg); btn.addEventListener('click', function() { - btn.disabled = true; + // aria-disabled instead of disabled, so keyboard focus stays on the button. + if (btn.getAttribute('aria-disabled') === 'true') return; + btn.setAttribute('aria-disabled', 'true'); msg.textContent = t('generating', 'Generating…'); + speak(msg.textContent); msg.className = 'everyalt-media-message'; fetch(restUrl + '/everyalt-api/v1/bulk_generate_alt', { @@ -45,11 +52,12 @@ return r.json(); }) .then(function(data) { - btn.disabled = false; + btn.removeAttribute('aria-disabled'); var success = data && data.success && (data.alt_text || data.decorative); if (success) { msg.textContent = data.decorative ? t('decorative', 'Marked as decorative: alt text left empty on purpose.') : t('generated', 'Alt text generated.'); msg.className = 'everyalt-media-message notice notice-success'; + speak(msg.textContent); var altField = document.getElementById('attachment_alt') || document.querySelector('textarea[name*="_wp_attachment_image_alt"]') || document.querySelector('input[name*="_wp_attachment_image_alt"]'); if (altField) { altField.value = data.alt_text; @@ -57,12 +65,14 @@ } else { msg.textContent = (data && data.message) ? data.message : t('failed', 'Could not generate alt text.'); msg.className = 'everyalt-media-message notice notice-error'; + speak(msg.textContent, true); } }) .catch(function(err) { - btn.disabled = false; + btn.removeAttribute('aria-disabled'); msg.textContent = t('errorPrefix', 'Error:') + ' ' + (err.message || t('failed', 'Could not generate alt text.')); msg.className = 'everyalt-media-message notice notice-error'; + speak(msg.textContent, true); }); }); })(); diff --git a/admin/partials/everyalt-options-display.php b/admin/partials/everyalt-options-display.php index 2dd4da6..1c4b965 100644 --- a/admin/partials/everyalt-options-display.php +++ b/admin/partials/everyalt-options-display.php @@ -6,18 +6,17 @@ * @subpackage EveryAlt/admin/partials */ $base_url = admin_url( 'upload.php?page=everyalt' ); +// Screen-reader notice for links that open a new tab. WordPress core's string, so it is already translated. +$everyalt_new_tab = ' ' . esc_html__( '(opens in a new tab)' ) . ''; ?>


- +

@@ -27,11 +26,14 @@ /* translators: 1: opening link to everyalt.com, 2: closing link tag, 3: opening link to hdc.net, 4: closing link tag */ __( '%1$sEveryAlt%2$s is a free, open-source project created by %3$sHDC%4$s, a web dev firm for high-stakes projects and AI builds.', 'everyalt' ), '', - '', + $everyalt_new_tab . '', '', - '' + $everyalt_new_tab . '' ), - array( 'a' => array( 'href' => true, 'target' => true, 'rel' => true ) ) + array( + 'a' => array( 'href' => true, 'target' => true, 'rel' => true ), + 'span' => array( 'class' => true ), + ) ); ?>

@@ -47,13 +49,13 @@

-