-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add popup support for <input>s styled as buttons and text fields
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
- Loading branch information
1 parent
204677b
commit 443bd26
Showing
1 changed file
with
179 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
|
@@ -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; | ||
|
@@ -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)"); |