diff --git a/CHANGELOG.md b/CHANGELOG.md index c4cb300a..385ee40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Next Release +* [#163](https://github.com/ruby-grape/grape-swagger-rails/pull/163): Copy to clipboard title - [@moskvin](https://github.com/moskvin). * Your contribution here. ### 1.0.2 (2026/05/26) diff --git a/app/assets/javascripts/grape_swagger_rails/index.js b/app/assets/javascripts/grape_swagger_rails/index.js index b5d594e2..9e4cd60a 100644 --- a/app/assets/javascripts/grape_swagger_rails/index.js +++ b/app/assets/javascripts/grape_swagger_rails/index.js @@ -1,4 +1,27 @@ "use strict"; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; +var COPY_BUTTON_LABEL = "Copy to clipboard"; +var labelCopyButtons = function (root) { + var targets = root instanceof Element && root.matches(".copy-to-clipboard") + ? __spreadArray([root], Array.from(root.querySelectorAll(".copy-to-clipboard")), true) : Array.from(root.querySelectorAll(".copy-to-clipboard")); + targets.forEach(function (node) { + var el = node; + if (!el.getAttribute("title")) { + el.setAttribute("title", COPY_BUTTON_LABEL); + } + if (!el.getAttribute("aria-label")) { + el.setAttribute("aria-label", COPY_BUTTON_LABEL); + } + }); +}; var safeDecodeURIComponent = function (value) { try { return decodeURIComponent(value); @@ -232,6 +255,24 @@ var initializeSwaggerPage = function () { bundleConfig.url = absoluteSpecUrl(options.url); } window.ui = SwaggerUIBundle(bundleConfig); + // Swagger UI's `.copy-to-clipboard` elements (the icon next to + // operation paths, cURL examples, OAuth redirect URLs, etc.) ship without + // a `title` or `aria-label` in some renderings, so on hover users see no + // tooltip and screen readers announce nothing. Label any unlabeled + // instances as Swagger UI renders or re-renders the tree. + labelCopyButtons(document.body); + var container = document.getElementById("swagger-ui-container"); + if (container) { + new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if (node.nodeType === Node.ELEMENT_NODE) { + labelCopyButtons(node); + } + }); + }); + }).observe(container, { childList: true, subtree: true }); + } if (selectedUrl) { window.ui.specActions.updateUrl(selectedUrl.url); window.ui.specActions.download(selectedUrl.url); diff --git a/app/assets/javascripts/grape_swagger_rails/index.min.js b/app/assets/javascripts/grape_swagger_rails/index.min.js index fdd7504b..8067264d 100644 --- a/app/assets/javascripts/grape_swagger_rails/index.min.js +++ b/app/assets/javascripts/grape_swagger_rails/index.min.js @@ -1,2 +1,2 @@ -"use strict";var safeDecodeURIComponent=function(o){try{return decodeURIComponent(o)}catch{return o}},initializeSwaggerPage=function(){var o=document.documentElement.dataset.swaggerOptions;if(!(!o||typeof SwaggerUIBundle>"u"||typeof SwaggerUIStandalonePreset>"u")){var t=JSON.parse(o),p=document.getElementById("input_apiKey"),s=document.getElementById("spec-selector"),m=document.getElementById("spec-selector-wrapper"),u=document.getElementById("theme-toggle"),g=document.documentElement,_=function(){return t.theme==="dark"?"dark":"light"},v=function(e){g.dataset.theme=e,g.classList.toggle("dark-mode",e==="dark"),u&&(u.textContent=e==="dark"?"Light Mode":"Dark Mode",u.setAttribute("aria-pressed",String(e==="dark")))},y=function(){if(!p)return"";var e=p.value?p.value.trim():"";return e?t.api_auth==="basic"?"Basic "+btoa(e):t.api_auth==="bearer"?"Bearer "+e:t.api_auth==="token"?'Token token="'.concat(e,'"'):e:""},I=function(e){return e.headers||(e.headers={}),e.headers},h=function(e,n,a){var r=I(e);if(r instanceof Headers){r.set(n,a);return}r[n]=a},d=function(e){return e?/^https?:\/\//.test(e)?e:t.app_url+e:""},S=function(){return Array.isArray(t.urls)?t.urls.map(function(e,n){return typeof e=="string"?{name:e,url:d(e),default:!1}:{name:e.name||e.url||"Spec "+(n+1),url:d(e.url),default:!!e.default}}).filter(function(e){return!!e.url}):[]},k=function(e){if(!e.length)return null;for(var n=0;n1&&s.addEventListener("change",function(e){var n=e.target,a=n.value;window.ui.specActions.updateUrl(a),window.ui.specActions.download(a)}),window.addEventListener("hashchange",function(){var e=window.location.hash;if(!(!e||e==="#")){var n=e.replace(/^#\/?/,"").split("/").filter(Boolean);if(n.length!==0){var a=safeDecodeURIComponent(n[0]),r=n.length>1?safeDecodeURIComponent(n[1]):null;window.ui.layoutActions.show(["operations-tag",a],!0),r&&window.ui.layoutActions.show(["operations",a,r],!0);var f=r?"operations-"+a+"-"+r:"operations-tag-"+a;requestAnimationFrame(function(){var w=document.getElementById(f);w&&w.scrollIntoView({behavior:"smooth",block:"start"})})}}})}};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",initializeSwaggerPage):initializeSwaggerPage(); +"use strict";var __spreadArray=this&&this.__spreadArray||function(o,t,l){if(l||arguments.length===2)for(var i=0,d=t.length,u;i"u"||typeof SwaggerUIStandalonePreset>"u")){var t=JSON.parse(o),l=document.getElementById("input_apiKey"),i=document.getElementById("spec-selector"),d=document.getElementById("spec-selector-wrapper"),u=document.getElementById("theme-toggle"),v=document.documentElement,_=function(){return t.theme==="dark"?"dark":"light"},h=function(e){v.dataset.theme=e,v.classList.toggle("dark-mode",e==="dark"),u&&(u.textContent=e==="dark"?"Light Mode":"Dark Mode",u.setAttribute("aria-pressed",String(e==="dark")))},b=function(){if(!l)return"";var e=l.value?l.value.trim():"";return e?t.api_auth==="basic"?"Basic "+btoa(e):t.api_auth==="bearer"?"Bearer "+e:t.api_auth==="token"?'Token token="'.concat(e,'"'):e:""},A=function(e){return e.headers||(e.headers={}),e.headers},m=function(e,r,n){var a=A(e);if(a instanceof Headers){a.set(r,n);return}a[r]=n},f=function(e){return e?/^https?:\/\//.test(e)?e:t.app_url+e:""},E=function(){return Array.isArray(t.urls)?t.urls.map(function(e,r){return typeof e=="string"?{name:e,url:f(e),default:!1}:{name:e.name||e.url||"Spec "+(r+1),url:f(e.url),default:!!e.default}}).filter(function(e){return!!e.url}):[]},S=function(e){if(!e.length)return null;for(var r=0;r1&&i.addEventListener("change",function(e){var r=e.target,n=r.value;window.ui.specActions.updateUrl(n),window.ui.specActions.download(n)}),window.addEventListener("hashchange",function(){var e=window.location.hash;if(!(!e||e==="#")){var r=e.replace(/^#\/?/,"").split("/").filter(Boolean);if(r.length!==0){var n=safeDecodeURIComponent(r[0]),a=r.length>1?safeDecodeURIComponent(r[1]):null;window.ui.layoutActions.show(["operations-tag",n],!0),a&&window.ui.layoutActions.show(["operations",n,a],!0);var g=a?"operations-"+n+"-"+a:"operations-tag-"+n;requestAnimationFrame(function(){var y=document.getElementById(g);y&&y.scrollIntoView({behavior:"smooth",block:"start"})})}}})}};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",initializeSwaggerPage):initializeSwaggerPage(); //# sourceMappingURL=index.min.js.map diff --git a/app/assets/javascripts/grape_swagger_rails/index.min.js.map b/app/assets/javascripts/grape_swagger_rails/index.min.js.map index fa66a359..6a243ccb 100644 --- a/app/assets/javascripts/grape_swagger_rails/index.min.js.map +++ b/app/assets/javascripts/grape_swagger_rails/index.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["index.js"], - "sourcesContent": ["\"use strict\";\nvar safeDecodeURIComponent = function (value) {\n try {\n return decodeURIComponent(value);\n }\n catch (_a) {\n return value;\n }\n};\nvar initializeSwaggerPage = function () {\n var optionsElement = document.documentElement.dataset.swaggerOptions;\n if (!optionsElement ||\n typeof SwaggerUIBundle === \"undefined\" ||\n typeof SwaggerUIStandalonePreset === \"undefined\") {\n return;\n }\n var options = JSON.parse(optionsElement);\n var authInput = document.getElementById(\"input_apiKey\");\n var specSelector = document.getElementById(\"spec-selector\");\n var specSelectorWrapper = document.getElementById(\"spec-selector-wrapper\");\n var themeToggle = document.getElementById(\"theme-toggle\");\n var root = document.documentElement;\n var getTheme = function () {\n return options.theme === \"dark\" ? \"dark\" : \"light\";\n };\n var applyTheme = function (theme) {\n root.dataset.theme = theme;\n root.classList.toggle(\"dark-mode\", theme === \"dark\");\n if (!themeToggle) {\n return;\n }\n themeToggle.textContent = theme === \"dark\" ? \"Light Mode\" : \"Dark Mode\";\n themeToggle.setAttribute(\"aria-pressed\", String(theme === \"dark\"));\n };\n var getApiKeyValue = function () {\n if (!authInput) {\n return \"\";\n }\n var key = authInput.value ? authInput.value.trim() : \"\";\n if (!key) {\n return \"\";\n }\n if (options.api_auth === \"basic\") {\n return \"Basic \" + btoa(key);\n }\n if (options.api_auth === \"bearer\") {\n return \"Bearer \" + key;\n }\n if (options.api_auth === \"token\") {\n return \"Token token=\\\"\".concat(key, \"\\\"\");\n }\n return key;\n };\n var ensureRequestHeaders = function (request) {\n if (!request.headers) {\n request.headers = {};\n }\n return request.headers;\n };\n var setRequestHeader = function (request, key, value) {\n var headers = ensureRequestHeaders(request);\n if (headers instanceof Headers) {\n headers.set(key, value);\n return;\n }\n headers[key] = value;\n };\n var absoluteSpecUrl = function (url) {\n if (!url) {\n return \"\";\n }\n if (/^https?:\\/\\//.test(url)) {\n return url;\n }\n return options.app_url + url;\n };\n var normalizeSwaggerUrls = function () {\n if (!Array.isArray(options.urls)) {\n return [];\n }\n return options.urls\n .map(function (entry, index) {\n if (typeof entry === \"string\") {\n return { name: entry, url: absoluteSpecUrl(entry), default: false };\n }\n return {\n name: entry.name || entry.url || \"Spec \" + (index + 1),\n url: absoluteSpecUrl(entry.url),\n default: Boolean(entry.default),\n };\n })\n .filter(function (entry) { return Boolean(entry.url); });\n };\n var selectedSwaggerUrl = function (urls) {\n if (!urls.length) {\n return null;\n }\n for (var i = 0; i < urls.length; i += 1) {\n if (urls[i].default) {\n return urls[i];\n }\n }\n if (options.url) {\n var absoluteUrl = absoluteSpecUrl(options.url);\n for (var j = 0; j < urls.length; j += 1) {\n if (urls[j].url === absoluteUrl) {\n return urls[j];\n }\n }\n }\n return urls[0];\n };\n var setupSpecSelector = function (urls, selectedUrl) {\n if (!specSelector || !specSelectorWrapper || urls.length < 2) {\n return;\n }\n urls.forEach(function (entry) {\n var option = document.createElement(\"option\");\n option.value = entry.url;\n option.textContent = entry.name;\n if (selectedUrl && entry.url === selectedUrl.url) {\n option.selected = true;\n }\n specSelector.appendChild(option);\n });\n specSelectorWrapper.hidden = false;\n };\n var hideInfoUrlPlugin = function () {\n return {\n wrapComponents: {\n InfoUrl: function () { return function () { return null; }; },\n },\n };\n };\n var hideDocVersionPlugin = function () {\n return {\n wrapComponents: {\n VersionStamp: function () { return function () { return null; }; },\n },\n };\n };\n var hideVersionStampPlugin = function () {\n return {\n wrapComponents: {\n OpenAPIVersion: function () { return function () { return null; }; },\n },\n };\n };\n // Swagger UI's \"Clear\" button (next to Execute) is confusing \u2014 it resets\n // the internal request / response state but does not clear values still visible\n // in the rendered input fields. Upstream issue swagger-api/swagger-ui#5283\n // has acknowledged the UX problem since 2019 without a fix. Hide by default.\n var hideClearButtonPlugin = function () {\n return {\n wrapComponents: {\n clear: function () { return function () { return null; }; },\n },\n };\n };\n var buildPlugins = function () {\n var configuredPlugins = options.swagger_ui_config && options.swagger_ui_config.plugins;\n var plugins = Array.isArray(configuredPlugins) ? configuredPlugins.slice() : [];\n var displayDefaults = {\n api_key_input: true,\n info_url: true,\n doc_version: true,\n version_stamp: true,\n clear_button: false,\n };\n var display = Object.assign({}, displayDefaults, options.display || {});\n if (!display.info_url) {\n plugins.push(hideInfoUrlPlugin);\n }\n if (!display.doc_version) {\n plugins.push(hideDocVersionPlugin);\n }\n if (!display.version_stamp) {\n plugins.push(hideVersionStampPlugin);\n }\n if (!display.clear_button) {\n plugins.push(hideClearButtonPlugin);\n }\n return plugins;\n };\n applyTheme(getTheme());\n if (themeToggle) {\n themeToggle.addEventListener(\"click\", function () {\n options.theme = root.dataset.theme === \"dark\" ? \"light\" : \"dark\";\n applyTheme(options.theme);\n });\n }\n var swaggerUrls = normalizeSwaggerUrls();\n var selectedUrl = selectedSwaggerUrl(swaggerUrls);\n var bundleConfig = Object.assign({}, options.swagger_ui_config || {}, {\n dom_id: \"#swagger-ui-container\",\n deepLinking: true,\n docExpansion: options.doc_expansion,\n supportedSubmitMethods: options.supported_submit_methods || [],\n validatorUrl: options.validator_url,\n layout: \"BaseLayout\",\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: buildPlugins(),\n requestInterceptor: function (request) {\n var headers = options.headers || {};\n Object.keys(headers).forEach(function (key) {\n setRequestHeader(request, key, headers[key]);\n });\n var apiKeyValue = getApiKeyValue();\n if (!apiKeyValue) {\n return request;\n }\n if (options.api_key_type === \"query\") {\n var separator = request.url.indexOf(\"?\") === -1 ? \"?\" : \"&\";\n request.url +=\n separator +\n encodeURIComponent(options.api_key_name) +\n \"=\" +\n encodeURIComponent(apiKeyValue);\n return request;\n }\n setRequestHeader(request, options.api_key_name, apiKeyValue);\n return request;\n },\n });\n if (swaggerUrls.length) {\n bundleConfig.urls = swaggerUrls;\n if (selectedUrl) {\n bundleConfig[\"urls.primaryName\"] = selectedUrl.name;\n }\n }\n else {\n bundleConfig.url = absoluteSpecUrl(options.url);\n }\n window.ui = SwaggerUIBundle(bundleConfig);\n if (selectedUrl) {\n window.ui.specActions.updateUrl(selectedUrl.url);\n window.ui.specActions.download(selectedUrl.url);\n }\n setupSpecSelector(swaggerUrls, selectedUrl);\n if (specSelector && swaggerUrls.length > 1) {\n specSelector.addEventListener(\"change\", function (event) {\n var target = event.target;\n var url = target.value;\n window.ui.specActions.updateUrl(url);\n window.ui.specActions.download(url);\n });\n }\n // Listen for hash changes so that navigating to a deep-link URL in the same\n // tab (e.g. pasting a copied operation URL into the address bar) expands the\n // target operation without requiring a full page refresh.\n //\n // NOTE: We intentionally do NOT use `layoutActions.parseDeepLinkHash()` here.\n // That method is Swagger UI's built-in deep linking plugin action, designed to\n // run during initial spec load (inside the `onComplete` callback). When called\n // after spec rendering is complete, it fails to expand operations \u2014 likely\n // because it depends on internal state or lifecycle context that no longer applies.\n //\n // Instead, we directly call `layoutActions.show()` which reliably toggles the\n // visibility of tags/operations, then scroll into view manually.\n window.addEventListener(\"hashchange\", function () {\n var hash = window.location.hash;\n if (!hash || hash === \"#\") {\n return;\n }\n // Hash format used by Swagger UI deep linking: #/tag/operationId\n var parts = hash.replace(/^#\\/?/, \"\").split(\"/\").filter(Boolean);\n if (parts.length === 0) {\n return;\n }\n var tag = safeDecodeURIComponent(parts[0]);\n var operationId = parts.length > 1 ? safeDecodeURIComponent(parts[1]) : null;\n window.ui.layoutActions.show([\"operations-tag\", tag], true);\n if (operationId) {\n window.ui.layoutActions.show([\"operations\", tag, operationId], true);\n }\n // Scroll to the expanded element. Swagger UI uses id=\"operations-{tag}-{operationId}\"\n // for operations and id=\"operations-tag-{tag}\" for tag sections. Use a small delay\n // to allow the DOM to update after the layout action.\n var targetId = operationId\n ? \"operations-\" + tag + \"-\" + operationId\n : \"operations-tag-\" + tag;\n requestAnimationFrame(function () {\n var element = document.getElementById(targetId);\n if (element) {\n element.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }\n });\n });\n};\nif (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", initializeSwaggerPage);\n}\nelse {\n initializeSwaggerPage();\n}\n"], - "mappings": "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", - "names": ["value", "optionsElement", "options", "authInput", "specSelector", "specSelectorWrapper", "themeToggle", "root", "getTheme", "applyTheme", "theme", "getApiKeyValue", "key", "ensureRequestHeaders", "request", "setRequestHeader", "headers", "absoluteSpecUrl", "url", "normalizeSwaggerUrls", "entry", "index", "selectedSwaggerUrl", "urls", "i", "absoluteUrl", "j", "setupSpecSelector", "selectedUrl", "option", "hideInfoUrlPlugin", "hideDocVersionPlugin", "hideVersionStampPlugin", "hideClearButtonPlugin", "buildPlugins", "configuredPlugins", "plugins", "displayDefaults", "display", "swaggerUrls", "bundleConfig", "apiKeyValue", "separator", "event", "target", "hash", "parts", "tag", "operationId", "targetId", "element"] + "sourcesContent": ["\"use strict\";\nvar __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n};\nvar COPY_BUTTON_LABEL = \"Copy to clipboard\";\nvar labelCopyButtons = function (root) {\n var targets = root instanceof Element && root.matches(\".copy-to-clipboard\")\n ? __spreadArray([root], Array.from(root.querySelectorAll(\".copy-to-clipboard\")), true) : Array.from(root.querySelectorAll(\".copy-to-clipboard\"));\n targets.forEach(function (node) {\n var el = node;\n if (!el.getAttribute(\"title\")) {\n el.setAttribute(\"title\", COPY_BUTTON_LABEL);\n }\n if (!el.getAttribute(\"aria-label\")) {\n el.setAttribute(\"aria-label\", COPY_BUTTON_LABEL);\n }\n });\n};\nvar safeDecodeURIComponent = function (value) {\n try {\n return decodeURIComponent(value);\n }\n catch (_a) {\n return value;\n }\n};\nvar initializeSwaggerPage = function () {\n var optionsElement = document.documentElement.dataset.swaggerOptions;\n if (!optionsElement ||\n typeof SwaggerUIBundle === \"undefined\" ||\n typeof SwaggerUIStandalonePreset === \"undefined\") {\n return;\n }\n var options = JSON.parse(optionsElement);\n var authInput = document.getElementById(\"input_apiKey\");\n var specSelector = document.getElementById(\"spec-selector\");\n var specSelectorWrapper = document.getElementById(\"spec-selector-wrapper\");\n var themeToggle = document.getElementById(\"theme-toggle\");\n var root = document.documentElement;\n var getTheme = function () {\n return options.theme === \"dark\" ? \"dark\" : \"light\";\n };\n var applyTheme = function (theme) {\n root.dataset.theme = theme;\n root.classList.toggle(\"dark-mode\", theme === \"dark\");\n if (!themeToggle) {\n return;\n }\n themeToggle.textContent = theme === \"dark\" ? \"Light Mode\" : \"Dark Mode\";\n themeToggle.setAttribute(\"aria-pressed\", String(theme === \"dark\"));\n };\n var getApiKeyValue = function () {\n if (!authInput) {\n return \"\";\n }\n var key = authInput.value ? authInput.value.trim() : \"\";\n if (!key) {\n return \"\";\n }\n if (options.api_auth === \"basic\") {\n return \"Basic \" + btoa(key);\n }\n if (options.api_auth === \"bearer\") {\n return \"Bearer \" + key;\n }\n if (options.api_auth === \"token\") {\n return \"Token token=\\\"\".concat(key, \"\\\"\");\n }\n return key;\n };\n var ensureRequestHeaders = function (request) {\n if (!request.headers) {\n request.headers = {};\n }\n return request.headers;\n };\n var setRequestHeader = function (request, key, value) {\n var headers = ensureRequestHeaders(request);\n if (headers instanceof Headers) {\n headers.set(key, value);\n return;\n }\n headers[key] = value;\n };\n var absoluteSpecUrl = function (url) {\n if (!url) {\n return \"\";\n }\n if (/^https?:\\/\\//.test(url)) {\n return url;\n }\n return options.app_url + url;\n };\n var normalizeSwaggerUrls = function () {\n if (!Array.isArray(options.urls)) {\n return [];\n }\n return options.urls\n .map(function (entry, index) {\n if (typeof entry === \"string\") {\n return { name: entry, url: absoluteSpecUrl(entry), default: false };\n }\n return {\n name: entry.name || entry.url || \"Spec \" + (index + 1),\n url: absoluteSpecUrl(entry.url),\n default: Boolean(entry.default),\n };\n })\n .filter(function (entry) { return Boolean(entry.url); });\n };\n var selectedSwaggerUrl = function (urls) {\n if (!urls.length) {\n return null;\n }\n for (var i = 0; i < urls.length; i += 1) {\n if (urls[i].default) {\n return urls[i];\n }\n }\n if (options.url) {\n var absoluteUrl = absoluteSpecUrl(options.url);\n for (var j = 0; j < urls.length; j += 1) {\n if (urls[j].url === absoluteUrl) {\n return urls[j];\n }\n }\n }\n return urls[0];\n };\n var setupSpecSelector = function (urls, selectedUrl) {\n if (!specSelector || !specSelectorWrapper || urls.length < 2) {\n return;\n }\n urls.forEach(function (entry) {\n var option = document.createElement(\"option\");\n option.value = entry.url;\n option.textContent = entry.name;\n if (selectedUrl && entry.url === selectedUrl.url) {\n option.selected = true;\n }\n specSelector.appendChild(option);\n });\n specSelectorWrapper.hidden = false;\n };\n var hideInfoUrlPlugin = function () {\n return {\n wrapComponents: {\n InfoUrl: function () { return function () { return null; }; },\n },\n };\n };\n var hideDocVersionPlugin = function () {\n return {\n wrapComponents: {\n VersionStamp: function () { return function () { return null; }; },\n },\n };\n };\n var hideVersionStampPlugin = function () {\n return {\n wrapComponents: {\n OpenAPIVersion: function () { return function () { return null; }; },\n },\n };\n };\n // Swagger UI's \"Clear\" button (next to Execute) is confusing \u2014 it resets\n // the internal request / response state but does not clear values still visible\n // in the rendered input fields. Upstream issue swagger-api/swagger-ui#5283\n // has acknowledged the UX problem since 2019 without a fix. Hide by default.\n var hideClearButtonPlugin = function () {\n return {\n wrapComponents: {\n clear: function () { return function () { return null; }; },\n },\n };\n };\n var buildPlugins = function () {\n var configuredPlugins = options.swagger_ui_config && options.swagger_ui_config.plugins;\n var plugins = Array.isArray(configuredPlugins) ? configuredPlugins.slice() : [];\n var displayDefaults = {\n api_key_input: true,\n info_url: true,\n doc_version: true,\n version_stamp: true,\n clear_button: false,\n };\n var display = Object.assign({}, displayDefaults, options.display || {});\n if (!display.info_url) {\n plugins.push(hideInfoUrlPlugin);\n }\n if (!display.doc_version) {\n plugins.push(hideDocVersionPlugin);\n }\n if (!display.version_stamp) {\n plugins.push(hideVersionStampPlugin);\n }\n if (!display.clear_button) {\n plugins.push(hideClearButtonPlugin);\n }\n return plugins;\n };\n applyTheme(getTheme());\n if (themeToggle) {\n themeToggle.addEventListener(\"click\", function () {\n options.theme = root.dataset.theme === \"dark\" ? \"light\" : \"dark\";\n applyTheme(options.theme);\n });\n }\n var swaggerUrls = normalizeSwaggerUrls();\n var selectedUrl = selectedSwaggerUrl(swaggerUrls);\n var bundleConfig = Object.assign({}, options.swagger_ui_config || {}, {\n dom_id: \"#swagger-ui-container\",\n deepLinking: true,\n docExpansion: options.doc_expansion,\n supportedSubmitMethods: options.supported_submit_methods || [],\n validatorUrl: options.validator_url,\n layout: \"BaseLayout\",\n presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],\n plugins: buildPlugins(),\n requestInterceptor: function (request) {\n var headers = options.headers || {};\n Object.keys(headers).forEach(function (key) {\n setRequestHeader(request, key, headers[key]);\n });\n var apiKeyValue = getApiKeyValue();\n if (!apiKeyValue) {\n return request;\n }\n if (options.api_key_type === \"query\") {\n var separator = request.url.indexOf(\"?\") === -1 ? \"?\" : \"&\";\n request.url +=\n separator +\n encodeURIComponent(options.api_key_name) +\n \"=\" +\n encodeURIComponent(apiKeyValue);\n return request;\n }\n setRequestHeader(request, options.api_key_name, apiKeyValue);\n return request;\n },\n });\n if (swaggerUrls.length) {\n bundleConfig.urls = swaggerUrls;\n if (selectedUrl) {\n bundleConfig[\"urls.primaryName\"] = selectedUrl.name;\n }\n }\n else {\n bundleConfig.url = absoluteSpecUrl(options.url);\n }\n window.ui = SwaggerUIBundle(bundleConfig);\n // Swagger UI's `.copy-to-clipboard` elements (the icon next to\n // operation paths, cURL examples, OAuth redirect URLs, etc.) ship without\n // a `title` or `aria-label` in some renderings, so on hover users see no\n // tooltip and screen readers announce nothing. Label any unlabeled\n // instances as Swagger UI renders or re-renders the tree.\n labelCopyButtons(document.body);\n var container = document.getElementById(\"swagger-ui-container\");\n if (container) {\n new MutationObserver(function (mutations) {\n mutations.forEach(function (mutation) {\n mutation.addedNodes.forEach(function (node) {\n if (node.nodeType === Node.ELEMENT_NODE) {\n labelCopyButtons(node);\n }\n });\n });\n }).observe(container, { childList: true, subtree: true });\n }\n if (selectedUrl) {\n window.ui.specActions.updateUrl(selectedUrl.url);\n window.ui.specActions.download(selectedUrl.url);\n }\n setupSpecSelector(swaggerUrls, selectedUrl);\n if (specSelector && swaggerUrls.length > 1) {\n specSelector.addEventListener(\"change\", function (event) {\n var target = event.target;\n var url = target.value;\n window.ui.specActions.updateUrl(url);\n window.ui.specActions.download(url);\n });\n }\n // Listen for hash changes so that navigating to a deep-link URL in the same\n // tab (e.g. pasting a copied operation URL into the address bar) expands the\n // target operation without requiring a full page refresh.\n //\n // NOTE: We intentionally do NOT use `layoutActions.parseDeepLinkHash()` here.\n // That method is Swagger UI's built-in deep linking plugin action, designed to\n // run during initial spec load (inside the `onComplete` callback). When called\n // after spec rendering is complete, it fails to expand operations \u2014 likely\n // because it depends on internal state or lifecycle context that no longer applies.\n //\n // Instead, we directly call `layoutActions.show()` which reliably toggles the\n // visibility of tags/operations, then scroll into view manually.\n window.addEventListener(\"hashchange\", function () {\n var hash = window.location.hash;\n if (!hash || hash === \"#\") {\n return;\n }\n // Hash format used by Swagger UI deep linking: #/tag/operationId\n var parts = hash.replace(/^#\\/?/, \"\").split(\"/\").filter(Boolean);\n if (parts.length === 0) {\n return;\n }\n var tag = safeDecodeURIComponent(parts[0]);\n var operationId = parts.length > 1 ? safeDecodeURIComponent(parts[1]) : null;\n window.ui.layoutActions.show([\"operations-tag\", tag], true);\n if (operationId) {\n window.ui.layoutActions.show([\"operations\", tag, operationId], true);\n }\n // Scroll to the expanded element. Swagger UI uses id=\"operations-{tag}-{operationId}\"\n // for operations and id=\"operations-tag-{tag}\" for tag sections. Use a small delay\n // to allow the DOM to update after the layout action.\n var targetId = operationId\n ? \"operations-\" + tag + \"-\" + operationId\n : \"operations-tag-\" + tag;\n requestAnimationFrame(function () {\n var element = document.getElementById(targetId);\n if (element) {\n element.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }\n });\n });\n};\nif (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", initializeSwaggerPage);\n}\nelse {\n initializeSwaggerPage();\n}\n"], + "mappings": "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", + "names": ["to", "from", "pack", "l", "ar", "root", "targets", "node", "el", "value", "optionsElement", "options", "authInput", "specSelector", "specSelectorWrapper", "themeToggle", "getTheme", "applyTheme", "theme", "getApiKeyValue", "key", "ensureRequestHeaders", "request", "setRequestHeader", "headers", "absoluteSpecUrl", "url", "normalizeSwaggerUrls", "entry", "index", "selectedSwaggerUrl", "urls", "i", "absoluteUrl", "j", "setupSpecSelector", "selectedUrl", "option", "hideInfoUrlPlugin", "hideDocVersionPlugin", "hideVersionStampPlugin", "hideClearButtonPlugin", "buildPlugins", "configuredPlugins", "plugins", "displayDefaults", "display", "swaggerUrls", "bundleConfig", "apiKeyValue", "separator", "container", "mutations", "mutation", "event", "target", "hash", "parts", "tag", "operationId", "targetId", "element"] } diff --git a/frontend/grape_swagger_rails/index.ts b/frontend/grape_swagger_rails/index.ts index 732ae28f..c44a2897 100644 --- a/frontend/grape_swagger_rails/index.ts +++ b/frontend/grape_swagger_rails/index.ts @@ -51,6 +51,25 @@ interface SwaggerPlugin { declare const SwaggerUIBundle: any; declare const SwaggerUIStandalonePreset: any; +const COPY_BUTTON_LABEL = "Copy to clipboard"; + +const labelCopyButtons = (root: ParentNode): void => { + const targets = + root instanceof Element && root.matches(".copy-to-clipboard") + ? [root, ...Array.from(root.querySelectorAll(".copy-to-clipboard"))] + : Array.from(root.querySelectorAll(".copy-to-clipboard")); + + targets.forEach((node) => { + const el = node as HTMLElement; + if (!el.getAttribute("title")) { + el.setAttribute("title", COPY_BUTTON_LABEL); + } + if (!el.getAttribute("aria-label")) { + el.setAttribute("aria-label", COPY_BUTTON_LABEL); + } + }); +}; + const safeDecodeURIComponent = (value: string): string => { try { return decodeURIComponent(value); @@ -339,6 +358,25 @@ const initializeSwaggerPage = (): void => { window.ui = SwaggerUIBundle(bundleConfig); + // Swagger UI's `.copy-to-clipboard` elements (the icon next to + // operation paths, cURL examples, OAuth redirect URLs, etc.) ship without + // a `title` or `aria-label` in some renderings, so on hover users see no + // tooltip and screen readers announce nothing. Label any unlabeled + // instances as Swagger UI renders or re-renders the tree. + labelCopyButtons(document.body); + const container = document.getElementById("swagger-ui-container"); + if (container) { + new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + labelCopyButtons(node as Element); + } + }); + }); + }).observe(container, { childList: true, subtree: true }); + } + if (selectedUrl) { window.ui.specActions.updateUrl(selectedUrl.url); window.ui.specActions.download(selectedUrl.url); diff --git a/spec/features/swagger_spec.rb b/spec/features/swagger_spec.rb index 48d6f1be..062b4015 100644 --- a/spec/features/swagger_spec.rb +++ b/spec/features/swagger_spec.rb @@ -123,6 +123,23 @@ def execute_operation(operation_id) end end + def find_unlabeled_copy_buttons + page.evaluate_script(<<~JS) + Array.from(document.querySelectorAll('.copy-to-clipboard')) + .filter(function(el) { + return !el.getAttribute('title') || !el.getAttribute('aria-label'); + }) + .map(function(el) { + return { + tag: el.tagName, + className: el.className, + title: el.getAttribute('title'), + ariaLabel: el.getAttribute('aria-label') + }; + }); + JS + end + def fill_echo_form within('#operations-echo-postApiEcho') do find('.try-out__btn').click @@ -257,6 +274,24 @@ def fill_echo_form end end + describe 'Copy button accessible label (swagger-ui#9584, #10699)' do + # Swagger UI's `.copy-to-clipboard` elements ship without a `title` or + # `aria-label` in several renderings, so hovering shows no tooltip and + # screen readers announce nothing. The TypeScript runtime labels them + # at boot and again as Swagger UI re-renders the tree. + + it 'labels every copy-to-clipboard element with title and aria-label' do + visit_swagger + open_operation('operations-tag-foos', 'operations-foos-getApiFoos') + expect(page).to have_css('.copy-to-clipboard', wait: 5) + + unlabeled = find_unlabeled_copy_buttons + + expect(unlabeled).to be_empty, + "Expected all .copy-to-clipboard elements to be labeled. Got: #{unlabeled.inspect}" + end + end + describe 'same-session deep link navigation' do # Regression tests for: navigating to a Swagger deep-link URL in the same # tab/session should expand the target operation without a full page refresh.