Skip to content

Commit

Permalink
Add popup support for <input>s styled as buttons and text fields
Browse files Browse the repository at this point in the history
With this CL, several <input> element types, in addition to the existing
support for <button>, will be able to use the invoking attributes
(togglepopup, showpopup, and hidepopup) to invoke a popup. This CL
also adds enforcement of the resolution below, so that any button
that would otherwise submit a form cannot also/instead trigger a
popup.

openui/open-ui#409 (comment)

Bug: 1307772
Change-Id: I7eb5cd726bcacd26de5085871d7c3077c1f78baf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3656513
Reviewed-by: David Baron <[email protected]>
Commit-Queue: David Baron <[email protected]>
Auto-Submit: Mason Freed <[email protected]>
Commit-Queue: Mason Freed <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1007708}
  • Loading branch information
mfreed7 authored and chromium-wpt-export-bot committed May 26, 2022
1 parent 0c02aca commit 98feb29
Showing 1 changed file with 179 additions and 84 deletions.
263 changes: 179 additions & 84 deletions html/semantics/popups/popup-invoking-attribute.tentative.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,190 @@
<title>Popup invoking attribute</title>
<link rel="author" href="mailto:[email protected]">
<link rel=help href="https://open-ui.org/components/popup.research.explainer">
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>

<button togglepopup=p1>Toggle Popup 1</button>
<div popup=popup id=p1>This is popup #1</div>

<style>
[popup] {
border: 5px solid red;
top: 100px;
left: 100px;
<body>
<iframe name="target"></iframe>
<script>
const buttonLogic = (t,s,h) => {
// This mimics the expected logic for button invokers:
let expectedBehavior = t ? "toggle" : (s ? "show" : (h ? "hide" : "none"));
let expectedId = t || s || h || 1;
if (!t && s && h) {
// Special case - only use toggle if the show/hide idrefs match.
expectedBehavior = (s === h) ? "toggle" : "show";
}
</style>
return {expectedBehavior, expectedId};
}
const textLogic = (t,s,h) => {
// This mimics the expected logic for text field invokers, which can
// only be shown via the down arrow, and never hidden.
return {expectedBehavior: (t || s) ? "show" : "none", expectedId: t || s || 1};
};
const noActivationLogic = (t,s,h) => {
// This does not activate any popups.
return {expectedBehavior: "none", expectedId: 1};
}
function activateTextInputFn(arrowChoice) {
return async (el) => {
// Press the down arrow
let key;
switch (arrowChoice) {
case 'down': key = '\uE015'; break; // ArrowDown
case 'right': key = '\uE014'; break; // ArrowRight
default: assert_unreached('invalid choice');
}
await new test_driver.send_keys(el,key);
};
}
function makeElementWithType(element,type) {
return (test) => {
const el = Object.assign(document.createElement(element),{type});
document.body.appendChild(el);
test.add_cleanup(() => el.remove());
return el;
};
}
const supportedButtonTypes = ['button','reset','submit',''].map(type => {
return {
name: `<button type="${type}">`,
makeElement: makeElementWithType('button',type),
invokeFn: el => el.click(),
getExpectedLogic: buttonLogic,
supported: true,
};
});
const supportedInputButtonTypes = ['button','reset','submit','image'].map(type => {
return {
name: `<input type="${type}"">`,
makeElement: makeElementWithType('input',type),
invokeFn: el => el.click(),
getExpectedLogic: buttonLogic,
supported: true,
};
});
const supportedTextTypes = ['text','email','password','search','tel','url'].map(type => {
return {
name: `<input type="${type}"">`,
makeElement: makeElementWithType('input',type),
invokeFn: activateTextInputFn('down'),
getExpectedLogic: textLogic, // Down arrow should work
supported: true,
};
});
const unsupportedTypes = ['checkbox','radio','range','file','color','date','datetime-local','month','time','week','number'].map(type => {
return {
name: `<input type="${type}"">`,
makeElement: makeElementWithType('input',type),
invokeFn: activateTextInputFn('down'),
getExpectedLogic: noActivationLogic, // None of these support popup invocation
supported: false,
};
});
const invokers = [
...supportedButtonTypes,
...supportedInputButtonTypes,
...supportedTextTypes,
...unsupportedTypes,
{
name: '<input type=text> with right arrow invocation',
makeElement: makeElementWithType('input','text'),
invokeFn: activateTextInputFn('right'),
getExpectedLogic: noActivationLogic, // Right arrow should not work
supported: false,
},
{
name: '<input type=text> focus only',
makeElement: makeElementWithType('input','text'),
invokeFn: el => el.focus(),
getExpectedLogic: noActivationLogic, // Just focusing the control should not work
supported: false,
},
];
["popup","hint","async"].forEach(type => {
invokers.forEach(testcase => {
let t_set = [1], s_set = [1], h_set = [1];
if (testcase.supported) {
t_set = s_set = h_set = [0,1,2]; // Test all permutations
}
t_set.forEach(t => {
s_set.forEach(s => {
h_set.forEach(h => {
promise_test(async test => {
const popup1 = Object.assign(document.createElement('div'),{popup: type, id: 'popup-1'});
const popup2 = Object.assign(document.createElement('div'),{popup: type, id: 'popup-2'});
assert_equals(popup1.popup,type);
assert_equals(popup2.popup,type);
assert_not_equals(popup1.id,popup2.id);
const invoker = testcase.makeElement(test);
if (t) invoker.setAttribute('togglepopup',t===1 ? popup1.id : popup2.id);
if (s) invoker.setAttribute('showpopup',s===1 ? popup1.id : popup2.id);
if (h) invoker.setAttribute('hidepopup',h===1 ? popup1.id : popup2.id);
assert_true(!document.getElementById(popup1.id));
assert_true(!document.getElementById(popup2.id));
document.body.appendChild(popup1);
document.body.appendChild(popup2);
test.add_cleanup(() => {
popup1.remove();
popup2.remove();
});
const {expectedBehavior, expectedId} = testcase.getExpectedLogic(t,s,h);
const otherId = expectedId !== 1 ? 1 : 2;
function assert_popup(num,state,message) {
assert_true(num>0,`Invalid expectedId ${num}`);
assert_equals((num===1 ? popup1 : popup2).matches(':popup-open'),state,message || "");
}
assert_popup(expectedId,false);
assert_popup(otherId,false);
await testcase.invokeFn(invoker);
assert_popup(otherId,false,'The other popup should never change');
switch (expectedBehavior) {
case "toggle":
case "show":
assert_popup(expectedId,true,'Toggle or show should show the popup');
(expectedId===1 ? popup1 : popup2).hidePopup(); // Hide the popup
break;
case "hide":
case "none":
assert_popup(expectedId,false,'Hide or none should leave the popup hidden');
break;
default:
assert_unreached();
}
(expectedId===1 ? popup1 : popup2).showPopup(); // Show the popup directly
assert_popup(expectedId,true);
assert_popup(otherId,false);
await testcase.invokeFn(invoker);
assert_popup(otherId,false,'The other popup should never change');
switch (expectedBehavior) {
case "toggle":
case "hide":
assert_popup(expectedId,false,'Toggle or hide should hide the popup');
break;
case "show":
case "none":
assert_popup(expectedId,true,'Show or none should leave the popup showing');
break;
default:
assert_unreached();
}
},`Test ${testcase.name}, t=${t}, s=${s}, h=${h}, with popup=${type}`);
});
});
});
});
});
</script>



<button togglepopup=p1>Toggle Popup 1</button>
<div popup=popup id=p1 style="border: 5px solid red;top: 100px;left: 100px;">This is popup #1</div>

<script>
function clickOn(element) {
Expand Down Expand Up @@ -54,7 +222,7 @@
await assertState(true,2,1);
popup.hidePopup();
await assertState(false,2,2);
}, "Clicking a togglepopup button opens a closed popup");
}, "Clicking a togglepopup button opens a closed popup (also check event counts)");

promise_test(async () => {
showCount = hideCount = 0;
Expand All @@ -63,77 +231,4 @@
await assertState(true,1,0);
await clickOn(button);
await assertState(false,1,1);
}, "Clicking a togglepopup button closes an open popup");

["popup","hint","async"].forEach(type => {
[0,1,2].forEach(t => {
[0,1,2].forEach(s => {
[0,1,2].forEach(h => {
const popup1 = Object.assign(document.createElement('div'),{popup: type, id: 'popup-1'});
const popup2 = Object.assign(document.createElement('div'),{popup: type, id: 'popup-2'});
assert_not_equals(popup1.id,popup2.id);
assert_true(!document.getElementById(popup1.id));
assert_true(!document.getElementById(popup2.id));
const button = document.createElement('button');
document.body.appendChild(popup1);
document.body.appendChild(popup2);
document.body.appendChild(button);
if (t) button.setAttribute('togglepopup',t===1 ? popup1.id : popup2.id);
if (s) button.setAttribute('showpopup',s===1 ? popup1.id : popup2.id);
if (h) button.setAttribute('hidepopup',h===1 ? popup1.id : popup2.id);
test(() => {
// This mimics the expected logic:
let expectedBehavior = t ? "toggle" : (s ? "show" : (h ? "hide" : "none"));
let expectedId = t || s || h || 1;
if (!t && s && h) {
// Special case - only use toggle if the show/hide idrefs match.
expectedBehavior = (s === h) ? "toggle" : "show";
}
const otherId = expectedId !== 1 ? 1 : 2;
function assert_popup(num,state,message) {
assert_true(num>0);
assert_equals((num===1 ? popup1 : popup2).matches(':popup-open'),state,message || "");
}
assert_popup(expectedId,false);
assert_popup(otherId,false);
button.click();
assert_popup(otherId,false,'The other popup should never change');
switch (expectedBehavior) {
case "toggle":
case "show":
assert_popup(expectedId,true,'Toggle or show should show the popup');
(expectedId===1 ? popup1 : popup2).hidePopup(); // Hide the popup
break;
case "hide":
case "none":
assert_popup(expectedId,false,'Hide or none should leave the popup hidden');
break;
default:
assert_unreached();
}
(expectedId===1 ? popup1 : popup2).showPopup(); // Show the popup
assert_popup(expectedId,true);
assert_popup(otherId,false);
button.click();
assert_popup(otherId,false,'The other popup should never change');
switch (expectedBehavior) {
case "toggle":
case "hide":
assert_popup(expectedId,false,'Toggle or hide should hide the popup');
break;
case "show":
case "none":
assert_popup(expectedId,true,'Show or none should leave the popup showing');
break;
default:
assert_unreached();
}
},`Test ${button.outerHTML} with popup=${type}`);
button.remove();
popup1.remove();
popup2.remove();
});
});
});
});
</script>
}, "Clicking a togglepopup button closes an open popup (also check event counts)");

0 comments on commit 98feb29

Please sign in to comment.