Bug 1249556 - Implement toggling details by keyboard. r=smaug

The user can switch to the main <summary> by tab key, and toggle the
<details> by either 'space' key or 'enter' key.

'return' key is handled with 'keypress', and the 'space' key is handled
with 'keyup' like the HTMLInputElement.

MozReview-Commit-ID: HE6IduUGCpj

--HG--
extra : rebase_source : 34598d95f35bf6b5bd927457ee09e42eb6ec0a68
This commit is contained in:
Ting-Yu Lin 2016-03-19 20:37:09 +08:00
parent 03310cda94
commit 22224242ba
11 changed files with 210 additions and 54 deletions

View File

@ -292,15 +292,8 @@ HTMLButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
eKeyPress == aVisitor.mEvent->mMessage) ||
(keyEvent->keyCode == NS_VK_SPACE &&
eKeyUp == aVisitor.mEvent->mMessage)) {
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(aVisitor.mEvent->mFlags.mIsTrusted,
eMouseClick, nullptr,
WidgetMouseEvent::eReal);
event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD;
EventDispatcher::Dispatch(static_cast<nsIContent*>(this),
aVisitor.mPresContext, &event, nullptr,
&status);
DispatchSimulatedClick(this, aVisitor.mEvent->mFlags.mIsTrusted,
aVisitor.mPresContext);
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
}

View File

@ -3824,15 +3824,8 @@ HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
case NS_FORM_INPUT_IMAGE: // Bug 34418
case NS_FORM_INPUT_COLOR:
{
WidgetMouseEvent event(aVisitor.mEvent->mFlags.mIsTrusted,
eMouseClick, nullptr,
WidgetMouseEvent::eReal);
event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD;
nsEventStatus status = nsEventStatus_eIgnore;
EventDispatcher::Dispatch(static_cast<nsIContent*>(this),
aVisitor.mPresContext, &event,
nullptr, &status);
DispatchSimulatedClick(this, aVisitor.mEvent->mFlags.mIsTrusted,
aVisitor.mPresContext);
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
} // case
} // switch
@ -3859,15 +3852,9 @@ HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
if (selectedRadioButton) {
rv = selectedRadioButton->Focus();
if (NS_SUCCEEDED(rv)) {
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(aVisitor.mEvent->mFlags.mIsTrusted,
eMouseClick, nullptr,
WidgetMouseEvent::eReal);
event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD;
rv =
EventDispatcher::Dispatch(ToSupports(selectedRadioButton),
aVisitor.mPresContext,
&event, nullptr, &status);
rv = DispatchSimulatedClick(selectedRadioButton,
aVisitor.mEvent->mFlags.mIsTrusted,
aVisitor.mPresContext);
if (NS_SUCCEEDED(rv)) {
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}

View File

@ -10,6 +10,8 @@
#include "mozilla/dom/HTMLUnknownElement.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/TextEvents.h"
#include "nsFocusManager.h"
// Expand NS_IMPL_NS_NEW_HTML_ELEMENT(Summary) to add pref check.
nsGenericHTMLElement*
@ -44,34 +46,99 @@ HTMLSummaryElement::PostHandleEvent(EventChainPostVisitor& aVisitor)
return rv;
}
auto toggleDetails = false;
auto* event = aVisitor.mEvent;
if (event->HasMouseEventMessage()) {
auto* mouseEvent = event->AsMouseEvent();
toggleDetails = mouseEvent->IsLeftClickEvent();
}
// Todo: Bug 634004: Implement toggle details by keyboard.
if (!toggleDetails || !IsMainSummary()) {
if (!IsMainSummary()) {
return rv;
}
auto* details = GetDetails();
MOZ_ASSERT(details, "Expected to find details since this is the main summary!");
WidgetEvent* const event = aVisitor.mEvent;
// When dispatching a synthesized mouse click event to a details with
// 'display: none', both Chrome and Safari do not toggle the 'open' attribute.
// We follow them by checking whether details has a frame or not.
if (details->GetPrimaryFrame()) {
details->ToggleOpen();
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
if (event->HasMouseEventMessage()) {
WidgetMouseEvent* mouseEvent = event->AsMouseEvent();
if (mouseEvent->IsLeftClickEvent()) {
RefPtr<HTMLDetailsElement> details = GetDetails();
MOZ_ASSERT(details,
"Expected to find details since this is the main summary!");
// When dispatching a synthesized mouse click event to a details element
// with 'display: none', both Chrome and Safari do not toggle the 'open'
// attribute. We follow them by checking whether the details element has a
// frame or not.
if (details->GetPrimaryFrame(Flush_Frames)) {
details->ToggleOpen();
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
return NS_OK;
}
}
} // event->HasMouseEventMessage()
if (event->HasKeyEventMessage()) {
WidgetKeyboardEvent* keyboardEvent = event->AsKeyboardEvent();
bool dispatchClick = false;
switch (event->mMessage) {
case eKeyPress:
if (keyboardEvent->charCode == nsIDOMKeyEvent::DOM_VK_SPACE) {
// Consume 'space' key to prevent scrolling the page down.
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
dispatchClick = keyboardEvent->keyCode == nsIDOMKeyEvent::DOM_VK_RETURN;
break;
case eKeyUp:
dispatchClick = keyboardEvent->keyCode == nsIDOMKeyEvent::DOM_VK_SPACE;
break;
default:
break;
}
if (dispatchClick) {
rv = DispatchSimulatedClick(this, event->mFlags.mIsTrusted,
aVisitor.mPresContext);
if (NS_SUCCEEDED(rv)) {
aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault;
}
}
} // event->HasKeyEventMessage()
return rv;
}
bool
HTMLSummaryElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
int32_t* aTabIndex)
{
bool disallowOverridingFocusability =
nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable, aTabIndex);
if (disallowOverridingFocusability || !IsMainSummary()) {
return disallowOverridingFocusability;
}
#ifdef XP_MACOSX
// The parent does not have strong opinion about the focusability of this main
// summary element, but we'd like to override it when mouse clicking on Mac OS
// like other form elements.
*aIsFocusable = !aWithMouse || nsFocusManager::sMouseFocusesFormControl;
#else
// The main summary element is focusable on other platforms.
*aIsFocusable = true;
#endif
// Give a chance to allow the subclass to override aIsFocusable.
return false;
}
int32_t
HTMLSummaryElement::TabIndexDefault()
{
// Make the main summary be able to navigate via tab, and be focusable.
// See nsGenericHTMLElement::IsHTMLFocusable().
return IsMainSummary() ? 0 : nsGenericHTMLElement::TabIndexDefault();
}
bool
HTMLSummaryElement::IsMainSummary() const
{

View File

@ -33,6 +33,11 @@ public:
nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override;
bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
int32_t* aTabIndex) override;
int32_t TabIndexDefault() override;
// Return true if this is the first summary element child of a details or the
// default summary element generated by DetailsFrame.
bool IsMainSummary() const;

View File

@ -2805,20 +2805,25 @@ nsGenericHTMLElement::PerformAccesskey(bool aKeyCausesActivation,
if (aKeyCausesActivation) {
// Click on it if the users prefs indicate to do so.
WidgetMouseEvent event(aIsTrustedEvent, eMouseClick, nullptr,
WidgetMouseEvent::eReal);
event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD;
nsAutoPopupStatePusher popupStatePusher(aIsTrustedEvent ?
openAllowed : openAbused);
EventDispatcher::Dispatch(static_cast<nsIContent*>(this),
presContext, &event);
DispatchSimulatedClick(this, aIsTrustedEvent, presContext);
}
return focused;
}
nsresult
nsGenericHTMLElement::DispatchSimulatedClick(nsGenericHTMLElement* aElement,
bool aIsTrusted,
nsPresContext* aPresContext)
{
WidgetMouseEvent event(aIsTrusted, eMouseClick, nullptr,
WidgetMouseEvent::eReal);
event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_KEYBOARD;
return EventDispatcher::Dispatch(ToSupports(aElement), aPresContext, &event);
}
const nsAttrName*
nsGenericHTMLElement::InternalGetExistingAttrNameFromQName(const nsAString& aStr) const
{

View File

@ -1027,6 +1027,13 @@ protected:
virtual const nsAttrName* InternalGetExistingAttrNameFromQName(const nsAString& aStr) const override;
/**
* Dispatch a simulated mouse click by keyboard to the given element.
*/
nsresult DispatchSimulatedClick(nsGenericHTMLElement* aElement,
bool aIsTrusted,
nsPresContext* aPresContext);
/**
* Create a URI for the given aURISpec string.
* Returns INVALID_STATE_ERR and nulls *aURI if aURISpec is empty

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<html class="reftest-wait">
<script>
function runTest() {
// Dispatch 'return' key on second summary should not collapse details.
var summary = document.getElementById("summary");
summary.dispatchEvent(new KeyboardEvent("keypress", {"keyCode": 13}));
document.documentElement.removeAttribute("class");
}
</script>
<body onload="runTest();">
<details open>
<summary>Summary</summary>
<summary id="summary">Second Summary</summary>
<p>This is the details.</p>
</details>
</body>
</html>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<html class="reftest-wait">
<script>
function runTest() {
var summary = document.getElementById("summary");
summary.addEventListener('click', function(e) {
// Prevent the details from toggling by key events.
e.preventDefault();
});
// Dispatch 'return' key to the summary element.
summary.dispatchEvent(new KeyboardEvent("keypress", {"keyCode": 13}));
document.documentElement.removeAttribute("class");
}
</script>
<body onload="runTest();">
<details>
<summary id="summary">Summary</summary>
<p>This is the details.</p>
</details>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<html class="reftest-wait">
<script>
function runTest() {
var summary = document.getElementById("summary");
// Dispatch 'return' key to the summary element.
summary.dispatchEvent(new KeyboardEvent("keypress", {"keyCode": 13}));
document.documentElement.removeAttribute("class");
}
</script>
<body onload="runTest();">
<details>
<summary id="summary">Summary</summary>
<p>This is the details.</p>
</details>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<html class="reftest-wait">
<script>
function runTest() {
var summary = document.getElementById("summary");
// Dispatch 'space' keys to the summary element.
summary.dispatchEvent(new KeyboardEvent("keydown", {"keyCode": 32}));
summary.dispatchEvent(new KeyboardEvent("keyup", {"keyCode": 32}));
document.documentElement.removeAttribute("class");
}
</script>
<body onload="runTest();">
<details>
<summary id="summary">Summary</summary>
<p>This is the details.</p>
</details>
</body>
</html>

View File

@ -72,3 +72,9 @@ pref(dom.details_element.enabled,true) == mouse-click-fixed-summary.html open-fi
pref(dom.details_element.enabled,true) == mouse-click-twice-fixed-summary.html fixed-summary.html
pref(dom.details_element.enabled,true) == mouse-click-float-details.html open-float-details.html
pref(dom.details_element.enabled,true) == mouse-click-twice-float-details.html float-details.html
# Dispatch keyboard event to summary
pref(dom.details_element.enabled,true) == key-enter-single-summary.html open-single-summary.html
pref(dom.details_element.enabled,true) == key-enter-open-second-summary.html open-multiple-summary.html
pref(dom.details_element.enabled,true) == key-enter-prevent-default.html single-summary.html
pref(dom.details_element.enabled,true) == key-space-single-summary.html open-single-summary.html