Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/components/auth/authorize-btn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class AuthorizeBtn extends React.Component {

return (
<div className="auth-wrapper">
<button className={isAuthorized ? "btn authorize locked" : "btn authorize unlocked"} onClick={onClick}>
<button type="button" className={isAuthorized ? "btn authorize locked" : "btn authorize unlocked"} onClick={onClick}>
<span>Authorize</span>
{isAuthorized ? <LockAuthIcon /> : <UnlockAuthIcon />}
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/auth/authorize-operation-btn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default class AuthorizeOperationBtn extends React.Component {
const UnlockAuthOperationIcon = getComponent("UnlockAuthOperationIcon", true)

return (
<button className="authorization__btn"
<button type="button" className="authorization__btn"
aria-label={isAuthorized ? "authorization button locked" : "authorization button unlocked"}
onClick={this.onClick}>
{isAuthorized ? <LockAuthOperationIcon className="locked" /> : <UnlockAuthOperationIcon className="unlocked"/>}
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/clear.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class Clear extends Component {

render(){
return (
<button className="btn btn-clear opblock-control__btn" onClick={ this.onClick }>
<button type="button" className="btn btn-clear opblock-control__btn" onClick={ this.onClick }>
Clear
</button>
)
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/execute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default class Execute extends Component {
render(){
const { disabled } = this.props
return (
<button className="btn execute opblock-control__btn" onClick={ this.onClick } disabled={disabled}>
<button type="button" className="btn execute opblock-control__btn" onClick={ this.onClick } disabled={disabled}>
Execute
</button>
)
Expand Down
4 changes: 2 additions & 2 deletions src/core/components/operation-summary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default class OperationSummary extends PureComponent {
const allowAnonymous = !hasSecurity || securityIsOptional
return (
<div className={`opblock-summary opblock-summary-${method}`} >
<button
<button type="button"
aria-expanded={isShown}
className="opblock-summary-control"
onClick={toggleShown}
Expand Down Expand Up @@ -94,7 +94,7 @@ export default class OperationSummary extends PureComponent {
/>
}
<JumpToPath path={specPath} />{/* TODO: use wrapComponents here, swagger-ui doesn't care about jumpToPath */}
<button
<button type="button"
aria-label={`${method} ${path.replace(/\//g, "\u200b/")}`}
className="opblock-control-arrow"
aria-expanded={isShown}
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/operation-tag.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export default class OperationTag extends React.Component {
}


<button
<button type="button"
aria-expanded={showTag}
className="expand-operation"
title={showTag ? "Collapse operation" : "Expand operation"}
Expand Down
6 changes: 3 additions & 3 deletions src/core/components/try-it-out-button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ export default class TryItOutButton extends React.Component {
return (
<div className={showReset ? "try-out btn-group" : "try-out"}>
{
enabled ? <button className="btn try-out__btn cancel" onClick={ onCancelClick }>Cancel</button>
: <button className="btn try-out__btn" onClick={ onTryoutClick }>Try it out </button>
enabled ? <button type="button" className="btn try-out__btn cancel" onClick={ onCancelClick }>Cancel</button>
: <button type="button" className="btn try-out__btn" onClick={ onTryoutClick }>Try it out </button>

}
{
showReset && <button className="btn try-out__btn reset" onClick={ onResetClick }>Reset</button>
showReset && <button type="button" className="btn try-out__btn reset" onClick={ onResetClick }>Reset</button>
}
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class ModelCollapse extends Component {

return (
<span className={classes || ""} ref={this.onLoad}>
<button aria-expanded={this.state.expanded} className="model-box-control" onClick={this.toggleCollapsed}>
<button type="button" aria-expanded={this.state.expanded} className="model-box-control" onClick={this.toggleCollapsed}>
{ title && <span className="pointer">{title}</span> }
<span className={ "model-toggle" + ( this.state.expanded ? "" : " collapsed" ) }></span>
{ !this.state.expanded && <span>{this.state.collapsedContent}</span> }
Expand Down
2 changes: 2 additions & 0 deletions src/core/plugins/json-schema-5/components/model-example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const ModelExample = ({
role="presentation"
>
<button
type="button"
aria-controls={examplePanelId}
aria-selected={activeTab === tabs.example}
className="tablinks"
Expand All @@ -88,6 +89,7 @@ const ModelExample = ({
role="presentation"
>
<button
type="button"
aria-controls={modelPanelId}
aria-selected={activeTab === tabs.model}
className={cx("tablinks", { inactive: isExecute })}
Expand Down
2 changes: 1 addition & 1 deletion src/core/plugins/json-schema-5/components/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class Models extends Component {

return <section className={ showModels ? "models is-open" : "models"} ref={this.onLoadModels}>
<h4>
<button
<button type="button"
aria-expanded={showModels}
className="models-control"
onClick={() => layoutActions.show(specPathBase, !showModels)}
Expand Down
1 change: 1 addition & 0 deletions src/core/plugins/oas31/components/models/models.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const Models = ({
>
<h4>
<button
type="button"
aria-expanded={isOpen}
className="models-control"
onClick={handleModelsExpand}
Expand Down
2 changes: 1 addition & 1 deletion src/core/plugins/request-snippets/request-snippets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent }) =>
onClick={() => handleSetIsExpanded()}
style={{ cursor: "pointer" }}
>Snippets</h4>
<button
<button type="button"
onClick={() => handleSetIsExpanded()}
style={{ border: "none", background: "none" }}
title={isExpanded ? "Collapse operation" : "Expand operation"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ const HighlightCode = ({
)}

{!downloadable ? null : (
<button className="download-contents" onClick={handleDownload}>
<button
type="button"
className="download-contents"
onClick={handleDownload}
>
Download
</button>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DarkModeToggle extends Component {

return (
<div className="dark-mode-toggle">
<button onClick={this.toggleIsDarkMode}>
<button type="button" onClick={this.toggleIsDarkMode}>
{!isDarkMode ? (
<LightBulbOff height="24" />
) : (
Expand Down
54 changes: 54 additions & 0 deletions test/e2e-cypress/e2e/bugs/10234.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Regression test for https://github.com/swagger-api/swagger-ui/issues/10234
*
* When Swagger UI is mounted inside a host application's <form>, clicking any
* native <button> rendered by Swagger UI must not trigger form submission.
* Native <button> elements default to type="submit", so every interactive
* Swagger UI button has to set type="button" explicitly.
*/
describe("#10234: native <button> elements default to type=\"button\" when nested in a host <form>", () => {
const visitHostFormPage = () => {
cy.visit("/pages/10234/")
// Allow Swagger UI to finish initializing.
cy.window().its("completeCount").should("be.greaterThan", 0)
// The flag must start as false; if anything causes a navigation or submit
// before we touch a button, this assertion will fail loudly.
cy.window().its("__swaggerUiHostFormSubmitted").should("eq", false)
}

const assertHostFormHasNotSubmitted = () => {
cy.window().its("__swaggerUiHostFormSubmitted").should("eq", false)
cy.location("pathname").should("eq", "/pages/10234/")
}

it("expanding a tag does not submit the host form", () => {
visitHostFormPage()
cy.get("button.expand-operation").first().click()
assertHostFormHasNotSubmitted()
})

it("opening an operation summary does not submit the host form", () => {
visitHostFormPage()
cy.get("button.opblock-summary-control").first().click()
assertHostFormHasNotSubmitted()
})

it("clicking the operation arrow does not submit the host form", () => {
visitHostFormPage()
cy.get("button.opblock-control-arrow").first().click()
assertHostFormHasNotSubmitted()
})

it("clicking 'Try it out' does not submit the host form", () => {
visitHostFormPage()
cy.get("button.opblock-summary-control").first().click()
cy.get("button.try-out__btn").first().click()
assertHostFormHasNotSubmitted()
})

it("clicking the Schemas/Models toggle does not submit the host form", () => {
visitHostFormPage()
cy.get("button.models-control").first().click()
assertHostFormHasNotSubmitted()
})
})
48 changes: 48 additions & 0 deletions test/e2e-cypress/static/pages/10234/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- Regression scenario for https://github.com/swagger-api/swagger-ui/issues/10234 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Swagger UI - issue 10234</title>
<link rel="stylesheet" type="text/css" href="/swagger-ui.css" >
<style>
html { box-sizing: border-box; overflow-y: scroll; }
*, *:before, *:after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
</style>
</head>

<body>
<!--
Swagger UI is mounted INSIDE a host <form>. Without type="button" on every
native <button>, clicking any of them defaults to type="submit" and triggers
navigation to /should-not-submit?... which the test asserts must not happen.
-->
<form id="host-form" action="/should-not-submit" method="get" onsubmit="window.__swaggerUiHostFormSubmitted = true; return false;">
<div id="swagger-ui"></div>
<input type="hidden" name="sentinel" value="please-do-not-submit" />
</form>

<script src="/swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script>
window.__swaggerUiHostFormSubmitted = false
window.onload = function () {
window["SwaggerUIBundle"] = window["swagger-ui-bundle"]
window["SwaggerUIStandalonePreset"] = window["swagger-ui-standalone-preset"]
const ui = SwaggerUIBundle({
url: "/documents/petstore-expanded.openapi.yaml",
dom_id: "#swagger-ui",
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: SwaggerUIStandalonePreset ? "StandaloneLayout" : "BaseLayout",
onComplete: function () {
window.completeCount = (window.completeCount || 0) + 1
}
})
window.ui = ui
}
</script>
</body>
</html>
141 changes: 141 additions & 0 deletions test/unit/components/a11y/button-type.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* @prettier
*
* Regression tests for https://github.com/swagger-api/swagger-ui/issues/10234
*
* When Swagger UI is rendered inside a host application's <form> element, any
* native <button> without an explicit `type` attribute defaults to
* `type="submit"` and triggers form submission on click. Every interactive
* <button> rendered by Swagger UI must therefore set `type="button"`.
*/
/* eslint-disable react/jsx-no-bind */
import React from "react"
import { shallow } from "enzyme"
import Im from "immutable"

import AuthorizeBtn from "core/components/auth/authorize-btn"
import AuthorizeOperationBtn from "core/components/auth/authorize-operation-btn"
import Clear from "core/components/clear"
import Execute from "core/components/execute"
import OperationTag from "core/components/operation-tag"
import TryItOutButton from "core/components/try-it-out-button"
import ModelCollapse from "core/plugins/json-schema-5/components/model-collapse"

const noop = () => null

describe('a11y: native <button> elements default to type="button"', function () {
it("AuthorizeBtn renders a non-submitting button", function () {
const wrapper = shallow(
<AuthorizeBtn
getComponent={() => noop}
onClick={() => {}}
isAuthorized={false}
showPopup={false}
/>
)
expect(wrapper.find("button").prop("type")).toEqual("button")
})

it("AuthorizeOperationBtn renders a non-submitting button", function () {
const wrapper = shallow(
<AuthorizeOperationBtn
getComponent={() => noop}
onClick={() => {}}
isAuthorized={false}
/>
)
expect(wrapper.find("button").prop("type")).toEqual("button")
})

it("Clear renders a non-submitting button", function () {
const wrapper = shallow(
<Clear
specActions={{ clearResponse: () => {}, clearRequest: () => {} }}
path="/foo"
method="get"
/>
)
expect(wrapper.find("button").prop("type")).toEqual("button")
})

it("Execute renders a non-submitting button", function () {
const wrapper = shallow(
<Execute
specSelectors={{
validateBeforeExecute: () => true,
}}
specActions={{
validateParams: () => {},
execute: () => {},
clearValidateParams: () => {},
}}
operation={Im.Map()}
path="/foo"
method="get"
oas3Selectors={{
requestBodyValue: () => null,
validateBeforeExecute: () => true,
requestContentType: () => null,
validateShallowRequired: () => [],
}}
oas3Actions={{
clearRequestBodyValidateError: () => {},
setRequestBodyValidateError: () => {},
}}
/>
)
expect(wrapper.find("button").prop("type")).toEqual("button")
})

it("OperationTag expand button is non-submitting", function () {
const wrapper = shallow(
<OperationTag
tagObj={Im.fromJS({})}
tag="pets"
oas3Selectors={() => null}
layoutSelectors={{
currentFilter: () => null,
isShown: () => false,
show: () => true,
}}
layoutActions={{ show: () => {} }}
getConfigs={() => ({})}
getComponent={() => noop}
specUrl=""
/>
)
expect(wrapper.find("button.expand-operation").prop("type")).toEqual(
"button"
)
})

it("TryItOutButton 'Try it out' is a non-submitting button", function () {
const wrapper = shallow(
<TryItOutButton enabled={false} isOAS3 hasUserEditedBody={false} />
)
expect(wrapper.find("button").prop("type")).toEqual("button")
})

it("TryItOutButton 'Cancel' and 'Reset' are non-submitting buttons", function () {
const wrapper = shallow(<TryItOutButton enabled isOAS3 hasUserEditedBody />)
const buttons = wrapper.find("button")
expect(buttons.length).toEqual(2)
buttons.forEach((node) => {
expect(node.prop("type")).toEqual("button")
})
})

it("ModelCollapse toggle button is non-submitting", function () {
const wrapper = shallow(
<ModelCollapse
layoutSelectors={{ getScrollToKey: () => Im.List() }}
specPath={Im.List([])}
>
<span>contents</span>
</ModelCollapse>
)
expect(wrapper.find("button.model-box-control").prop("type")).toEqual(
"button"
)
})
})