Bug 501496 part.13 nsListControlFrame should handle key navigation at keydown event handler r=neil

This commit is contained in:
Masayuki Nakano 2013-07-25 15:09:29 +09:00
Родитель 632a4f781f
Коммит 345d20234e
4 изменённых файлов: 253 добавлений и 213 удалений

Просмотреть файл

@ -142,6 +142,8 @@ nsListControlFrame::DestroyFrom(nsIFrame* aDestructRoot)
mEventListener->SetFrame(nullptr);
mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keydown"),
mEventListener, false);
mContent->RemoveSystemEventListener(NS_LITERAL_STRING("keypress"),
mEventListener, false);
mContent->RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"),
@ -996,6 +998,8 @@ nsListControlFrame::Init(nsIContent* aContent,
// we need to hook up our listeners before the editor is initialized
mEventListener = new nsListEventListener(this);
mContent->AddSystemEventListener(NS_LITERAL_STRING("keydown"),
mEventListener, false, false);
mContent->AddSystemEventListener(NS_LITERAL_STRING("keypress"),
mEventListener, false, false);
mContent->AddSystemEventListener(NS_LITERAL_STRING("mousedown"),
@ -2216,42 +2220,31 @@ nsListControlFrame::DropDownToggleKey(nsIDOMEvent* aKeyEvent)
}
nsresult
nsListControlFrame::KeyPress(nsIDOMEvent* aKeyEvent)
nsListControlFrame::KeyDown(nsIDOMEvent* aKeyEvent)
{
NS_ASSERTION(aKeyEvent, "keyEvent is null.");
MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
nsEventStates eventStates = mContent->AsElement()->State();
if (eventStates.HasState(NS_EVENT_STATE_DISABLED))
if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
return NS_OK;
}
// Start by making sure we can query for a key event
nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aKeyEvent);
NS_ENSURE_TRUE(keyEvent, NS_ERROR_FAILURE);
// Don't check defaultPrevented value because other browsers don't prevent
// the key navigation of list control even if preventDefault() is called.
uint32_t keycode = 0;
uint32_t charcode = 0;
keyEvent->GetKeyCode(&keycode);
keyEvent->GetCharCode(&charcode);
const nsKeyEvent* keyEvent =
static_cast<nsKeyEvent*>(aKeyEvent->GetInternalNSEvent());
MOZ_ASSERT(keyEvent, "DOM event must have internal event");
MOZ_ASSERT(keyEvent->eventStructType == NS_KEY_EVENT,
"The keydown event's internal event struct must be nsKeyEvent");
bool isAlt = false;
keyEvent->GetAltKey(&isAlt);
if (isAlt) {
if (keycode == nsIDOMKeyEvent::DOM_VK_UP || keycode == nsIDOMKeyEvent::DOM_VK_DOWN) {
if (keyEvent->IsAlt()) {
if (keyEvent->keyCode == NS_VK_UP || keyEvent->keyCode == NS_VK_DOWN) {
DropDownToggleKey(aKeyEvent);
}
return NS_OK;
}
// Get control / shift modifiers
bool isControl = false;
bool isShift = false;
keyEvent->GetCtrlKey(&isControl);
if (!isControl) {
keyEvent->GetMetaKey(&isControl);
}
keyEvent->GetShiftKey(&isShift);
// now make sure there are options or we are wasting our time
nsCOMPtr<nsIDOMHTMLOptionsCollection> options = GetOptions(mContent);
NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
@ -2259,246 +2252,286 @@ nsListControlFrame::KeyPress(nsIDOMEvent* aKeyEvent)
uint32_t numOptions = 0;
options->GetLength(&numOptions);
// Whether we did an incremental search or another action
bool didIncrementalSearch = false;
// this is the new index to set
// DOM_VK_RETURN & DOM_VK_ESCAPE will not set this
int32_t newIndex = kNothingSelected;
// set up the old and new selected index and process it
// DOM_VK_RETURN selects the item
// DOM_VK_ESCAPE cancels the selection
// default processing checks to see if the pressed the first
// letter of an item in the list and advances to it
if (isControl && (keycode == nsIDOMKeyEvent::DOM_VK_UP ||
keycode == nsIDOMKeyEvent::DOM_VK_LEFT ||
keycode == nsIDOMKeyEvent::DOM_VK_DOWN ||
keycode == nsIDOMKeyEvent::DOM_VK_RIGHT)) {
bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
if (isControlOrMeta && (keyEvent->keyCode == NS_VK_UP ||
keyEvent->keyCode == NS_VK_LEFT ||
keyEvent->keyCode == NS_VK_DOWN ||
keyEvent->keyCode == NS_VK_RIGHT)) {
// Don't go into multiple select mode unless this list can handle it
isControl = mControlSelectMode = GetMultiple();
} else if (charcode != ' ') {
isControlOrMeta = mControlSelectMode = GetMultiple();
} else if (keyEvent->keyCode != NS_VK_SPACE) {
mControlSelectMode = false;
}
switch (keycode) {
case nsIDOMKeyEvent::DOM_VK_UP:
case nsIDOMKeyEvent::DOM_VK_LEFT: {
switch (keyEvent->keyCode) {
case NS_VK_UP:
case NS_VK_LEFT:
AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
(int32_t)numOptions,
static_cast<int32_t>(numOptions),
-1, -1);
} break;
case nsIDOMKeyEvent::DOM_VK_DOWN:
case nsIDOMKeyEvent::DOM_VK_RIGHT: {
break;
case NS_VK_DOWN:
case NS_VK_RIGHT:
AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
(int32_t)numOptions,
static_cast<int32_t>(numOptions),
1, 1);
} break;
case nsIDOMKeyEvent::DOM_VK_RETURN: {
if (mComboboxFrame != nullptr) {
break;
case NS_VK_RETURN:
if (mComboboxFrame) {
if (mComboboxFrame->IsDroppedDown()) {
nsWeakFrame weakFrame(this);
ComboboxFinish(mEndSelectionIndex);
if (!weakFrame.IsAlive())
if (!weakFrame.IsAlive()) {
return NS_OK;
}
}
FireOnChange();
return NS_OK;
} else {
newIndex = mEndSelectionIndex;
}
} break;
case nsIDOMKeyEvent::DOM_VK_ESCAPE: {
newIndex = mEndSelectionIndex;
break;
case NS_VK_ESCAPE: {
nsWeakFrame weakFrame(this);
AboutToRollup();
if (!weakFrame.IsAlive()) {
aKeyEvent->PreventDefault(); // since we won't reach the one below
return NS_OK;
}
} break;
case nsIDOMKeyEvent::DOM_VK_PAGE_UP: {
break;
}
case NS_VK_PAGE_UP: {
int32_t itemsPerPage =
std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
(int32_t)numOptions,
-std::max(1, int32_t(mNumDisplayRows-1)), -1);
} break;
case nsIDOMKeyEvent::DOM_VK_PAGE_DOWN: {
static_cast<int32_t>(numOptions),
-itemsPerPage, -1);
break;
}
case NS_VK_PAGE_DOWN: {
int32_t itemsPerPage =
std::max(1, static_cast<int32_t>(mNumDisplayRows - 1));
AdjustIndexForDisabledOpt(mEndSelectionIndex, newIndex,
(int32_t)numOptions,
std::max(1, int32_t(mNumDisplayRows-1)), 1);
} break;
case nsIDOMKeyEvent::DOM_VK_HOME: {
static_cast<int32_t>(numOptions),
itemsPerPage, 1);
break;
}
case NS_VK_HOME:
AdjustIndexForDisabledOpt(0, newIndex,
(int32_t)numOptions,
static_cast<int32_t>(numOptions),
0, 1);
} break;
case nsIDOMKeyEvent::DOM_VK_END: {
AdjustIndexForDisabledOpt(numOptions-1, newIndex,
(int32_t)numOptions,
break;
case NS_VK_END:
AdjustIndexForDisabledOpt(static_cast<int32_t>(numOptions) - 1, newIndex,
static_cast<int32_t>(numOptions),
0, -1);
} break;
break;
#if defined(XP_WIN) || defined(XP_OS2)
case nsIDOMKeyEvent::DOM_VK_F4: {
case NS_VK_F4:
DropDownToggleKey(aKeyEvent);
return NS_OK;
} break;
#endif
case nsIDOMKeyEvent::DOM_VK_TAB: {
default: // printable key will be handled by keypress event.
return NS_OK;
}
aKeyEvent->PreventDefault();
// Cancel incremental search if it's being performed.
GetIncrementalString().Truncate();
// Actually process the new index and let the selection code
// do the scrolling for us
PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta);
return NS_OK;
}
nsresult
nsListControlFrame::KeyPress(nsIDOMEvent* aKeyEvent)
{
MOZ_ASSERT(aKeyEvent, "aKeyEvent is null.");
nsEventStates eventStates = mContent->AsElement()->State();
if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
return NS_OK;
}
const nsKeyEvent* keyEvent =
static_cast<nsKeyEvent*>(aKeyEvent->GetInternalNSEvent());
MOZ_ASSERT(keyEvent, "DOM event must have internal event");
MOZ_ASSERT(keyEvent->eventStructType == NS_KEY_EVENT,
"The keydown event's internal event struct must be nsKeyEvent");
// Select option with this as the first character
// XXX Not I18N compliant
// Don't do incremental search if the key event has already consumed.
if (keyEvent->mFlags.mDefaultPrevented) {
return NS_OK;
}
if (keyEvent->IsAlt()) {
return NS_OK;
}
// With some keyboard layout, space key causes non-ASCII space.
// So, the check in keydown event handler isn't enough, we need to check it
// again with keypress event.
if (keyEvent->charCode != ' ') {
mControlSelectMode = false;
}
bool isControlOrMeta = (keyEvent->IsControl() || keyEvent->IsMeta());
if (isControlOrMeta && keyEvent->charCode != ' ') {
return NS_OK;
}
// NOTE: If keyCode of keypress event is not 0, charCode is always 0.
// Therefore, all non-printable keys are not handled after this block.
if (!keyEvent->charCode) {
// Backspace key will delete the last char in the string
// XXX Backspace key causes "go back the history" on Windows. Shouldn't we
// prevent its default action if incremental search is used since
// getting focus? When I tested this, it worked accidentally.
if (keyEvent->keyCode == NS_VK_BACK && !GetIncrementalString().IsEmpty()) {
GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
aKeyEvent->PreventDefault();
}
default: { // Select option with this as the first character
// XXX Not I18N compliant
// We skip processing all key events in the keys that are not in the
// cases above, if they have been prevented. The keys listed in the cases
// above are required to navigate inside the list. These keys are also
// not prevented in most UAs, so this is also a compatibility issue.
bool defaultPrevented;
aKeyEvent->GetDefaultPrevented(&defaultPrevented);
if (defaultPrevented) {
return NS_OK;
}
if (isControl && charcode != ' ') {
return NS_OK;
}
didIncrementalSearch = true;
if (charcode == 0) {
// Backspace key will delete the last char in the string
if (keycode == NS_VK_BACK && !GetIncrementalString().IsEmpty()) {
GetIncrementalString().Truncate(GetIncrementalString().Length() - 1);
aKeyEvent->PreventDefault();
}
return NS_OK;
}
DOMTimeStamp keyTime;
aKeyEvent->GetTimeStamp(&keyTime);
// Incremental Search: if time elapsed is below
// INCREMENTAL_SEARCH_KEYPRESS_TIME, append this keystroke to the search
// string we will use to find options and start searching at the current
// keystroke. Otherwise, Truncate the string if it's been a long time
// since our last keypress.
if (keyTime - gLastKeyTime > INCREMENTAL_SEARCH_KEYPRESS_TIME) {
// If this is ' ' and we are at the beginning of the string, treat it as
// "select this option" (bug 191543)
if (charcode == ' ') {
newIndex = mEndSelectionIndex;
break;
}
GetIncrementalString().Truncate();
}
gLastKeyTime = keyTime;
// Append this keystroke to the search string.
PRUnichar uniChar = ToLowerCase(static_cast<PRUnichar>(charcode));
GetIncrementalString().Append(uniChar);
// See bug 188199, if all letters in incremental string are same, just try to match the first one
nsAutoString incrementalString(GetIncrementalString());
uint32_t charIndex = 1, stringLength = incrementalString.Length();
while (charIndex < stringLength && incrementalString[charIndex] == incrementalString[charIndex - 1]) {
charIndex++;
}
if (charIndex == stringLength) {
incrementalString.Truncate(1);
stringLength = 1;
}
// Determine where we're going to start reading the string
// If we have multiple characters to look for, we start looking *at* the
// current option. If we have only one character to look for, we start
// looking *after* the current option.
// Exception: if there is no option selected to start at, we always start
// *at* 0.
int32_t startIndex = GetSelectedIndex();
if (startIndex == kNothingSelected) {
startIndex = 0;
} else if (stringLength == 1) {
startIndex++;
}
for (uint32_t i = 0; i < numOptions; ++i) {
uint32_t index = (i + startIndex) % numOptions;
nsCOMPtr<nsIDOMHTMLOptionElement> optionElement = GetOption(options, index);
if (!optionElement) {
continue;
}
nsAutoString text;
if (NS_FAILED(optionElement->GetText(text)) ||
!StringBeginsWith(nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
incrementalString,
nsCaseInsensitiveStringComparator())) {
continue;
}
if (!PerformSelection(index, isShift, isControl)) {
break;
}
// If UpdateSelection() returns false, that means the frame is no longer
// alive. We should stop doing anything.
if (!UpdateSelection()) {
return NS_OK;
}
break;
}
} break;//case
} // switch
return NS_OK;
}
// We ate the key if we got this far.
aKeyEvent->PreventDefault();
// If we didn't do an incremental search, clear the string
if (!didIncrementalSearch) {
// XXX Why don't we check/modify timestamp first?
// Incremental Search: if time elapsed is below
// INCREMENTAL_SEARCH_KEYPRESS_TIME, append this keystroke to the search
// string we will use to find options and start searching at the current
// keystroke. Otherwise, Truncate the string if it's been a long time
// since our last keypress.
if (keyEvent->time - gLastKeyTime > INCREMENTAL_SEARCH_KEYPRESS_TIME) {
// If this is ' ' and we are at the beginning of the string, treat it as
// "select this option" (bug 191543)
if (keyEvent->charCode == ' ') {
// Actually process the new index and let the selection code
// do the scrolling for us
PostHandleKeyEvent(mEndSelectionIndex, keyEvent->charCode,
keyEvent->IsShift(), isControlOrMeta);
return NS_OK;
}
GetIncrementalString().Truncate();
}
// Actually process the new index and let the selection code
// do the scrolling for us
if (newIndex != kNothingSelected) {
// If you hold control, but not shift, no key will actually do anything
// except space.
bool wasChanged = false;
if (isControl && !isShift && charcode != ' ') {
mStartSelectionIndex = newIndex;
mEndSelectionIndex = newIndex;
InvalidateFocus();
ScrollToIndex(newIndex);
gLastKeyTime = keyEvent->time;
#ifdef ACCESSIBILITY
FireMenuItemActiveEvent();
#endif
} else if (mControlSelectMode && charcode == ' ') {
wasChanged = SingleSelection(newIndex, true);
} else {
wasChanged = PerformSelection(newIndex, isShift, isControl);
// Append this keystroke to the search string.
PRUnichar uniChar = ToLowerCase(static_cast<PRUnichar>(keyEvent->charCode));
GetIncrementalString().Append(uniChar);
// See bug 188199, if all letters in incremental string are same, just try to
// match the first one
nsAutoString incrementalString(GetIncrementalString());
uint32_t charIndex = 1, stringLength = incrementalString.Length();
while (charIndex < stringLength &&
incrementalString[charIndex] == incrementalString[charIndex - 1]) {
charIndex++;
}
if (charIndex == stringLength) {
incrementalString.Truncate(1);
stringLength = 1;
}
// Determine where we're going to start reading the string
// If we have multiple characters to look for, we start looking *at* the
// current option. If we have only one character to look for, we start
// looking *after* the current option.
// Exception: if there is no option selected to start at, we always start
// *at* 0.
int32_t startIndex = GetSelectedIndex();
if (startIndex == kNothingSelected) {
startIndex = 0;
} else if (stringLength == 1) {
startIndex++;
}
// now make sure there are options or we are wasting our time
nsCOMPtr<nsIDOMHTMLOptionsCollection> options = GetOptions(mContent);
NS_ENSURE_TRUE(options, NS_ERROR_FAILURE);
uint32_t numOptions = 0;
options->GetLength(&numOptions);
for (uint32_t i = 0; i < numOptions; ++i) {
uint32_t index = (i + startIndex) % numOptions;
nsCOMPtr<nsIDOMHTMLOptionElement> optionElement = GetOption(options, index);
if (!optionElement) {
continue;
}
if (wasChanged) {
// dispatch event, update combobox, etc.
if (!UpdateSelection()) {
return NS_OK;
}
nsAutoString text;
if (NS_FAILED(optionElement->GetText(text)) ||
!StringBeginsWith(
nsContentUtils::TrimWhitespace<
nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false),
incrementalString, nsCaseInsensitiveStringComparator())) {
continue;
}
if (!PerformSelection(index, keyEvent->IsShift(), isControlOrMeta)) {
break;
}
// If UpdateSelection() returns false, that means the frame is no longer
// alive. We should stop doing anything.
if (!UpdateSelection()) {
return NS_OK;
}
break;
}
return NS_OK;
}
void
nsListControlFrame::PostHandleKeyEvent(int32_t aNewIndex,
uint32_t aCharCode,
bool aIsShift,
bool aIsControlOrMeta)
{
if (aNewIndex == kNothingSelected) {
return;
}
// If you hold control, but not shift, no key will actually do anything
// except space.
bool wasChanged = false;
if (aIsControlOrMeta && !aIsShift && aCharCode != ' ') {
mStartSelectionIndex = aNewIndex;
mEndSelectionIndex = aNewIndex;
InvalidateFocus();
ScrollToIndex(aNewIndex);
#ifdef ACCESSIBILITY
FireMenuItemActiveEvent();
#endif
} else if (mControlSelectMode && aCharCode == ' ') {
wasChanged = SingleSelection(aNewIndex, true);
} else {
wasChanged = PerformSelection(aNewIndex, aIsShift, aIsControlOrMeta);
}
if (wasChanged) {
// dispatch event, update combobox, etc.
UpdateSelection();
}
}
/******************************************************************************
* nsListEventListener
@ -2514,6 +2547,8 @@ nsListEventListener::HandleEvent(nsIDOMEvent* aEvent)
nsAutoString eventType;
aEvent->GetType(eventType);
if (eventType.EqualsLiteral("keydown"))
return mFrame->nsListControlFrame::KeyDown(aEvent);
if (eventType.EqualsLiteral("keypress"))
return mFrame->nsListControlFrame::KeyPress(aEvent);
if (eventType.EqualsLiteral("mousedown"))

Просмотреть файл

@ -160,6 +160,7 @@ public:
nsresult MouseUp(nsIDOMEvent* aMouseEvent); // might destroy |this|
nsresult MouseMove(nsIDOMEvent* aMouseEvent);
nsresult DragMove(nsIDOMEvent* aMouseEvent);
nsresult KeyDown(nsIDOMEvent* aKeyEvent); // might destroy |this|
nsresult KeyPress(nsIDOMEvent* aKeyEvent); // might destroy |this|
/**
@ -375,6 +376,8 @@ protected:
bool aIsControl);
bool HandleListSelection(nsIDOMEvent * aDOMEvent, int32_t selectedIndex);
void InitSelectionRange(int32_t aClickedIndex);
void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode,
bool aIsShift, bool aIsControlOrMeta);
public:
nsSelectsAreaFrame* GetOptionsContainer() const {

Просмотреть файл

@ -33,11 +33,11 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=291082
element.focus();
var previousValue = element.value;
sendChar('2');
is(element.value, previousValue, "value should not have changed");
is(element.value, previousValue, "value should not have changed (id: " + id + ")");
previousValue = element.value;
otherKeys.forEach(function(key) {
sendKey(key);
isnot(element.value, previousValue, "value should have changed while testing key " + key);
isnot(element.value, previousValue, "value should have changed while testing key " + key + " (id: " + id + ")");
previousValue = element.value;
});
});

Просмотреть файл

@ -451,6 +451,8 @@ function _computeKeyCodeFromChar(aChar)
return nsIDOMKeyEvent.DOM_VK_SLASH;
case '\n':
return nsIDOMKeyEvent.DOM_VK_RETURN;
case ' ':
return nsIDOMKeyEvent.DOM_VK_SPACE;
default:
return 0;
}