diff --git a/css/80_app.css b/css/80_app.css index 13f256df46f..dd3c7bb3968 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -2877,6 +2877,13 @@ img.tag-reference-wiki-image { .section-raw-membership-editor .member-list { position: relative; /* required for drag-and-drop */ padding-top: 5px; + overflow-y: auto; + max-height: 400px; +} +.section-raw-member-editor .member-list:focus, +.section-raw-membership-editor .member-list:focus { + outline: 2px solid #0066ff; + outline-offset: -2px; } .section-raw-member-editor .member-list li, .section-raw-membership-editor .member-list li { diff --git a/modules/ui/sections/raw_member_editor.js b/modules/ui/sections/raw_member_editor.js index 8671760243d..34f09014bfe 100644 --- a/modules/ui/sections/raw_member_editor.js +++ b/modules/ui/sections/raw_member_editor.js @@ -144,6 +144,12 @@ export function uiSectionRawMemberEditor(context) { list = list.enter() .append('ul') .attr('class', 'member-list') + .attr('tabindex', '0') + .on('mousedown.memberlist', function(event) { + this.focus(); + event.stopPropagation(); + }) + .on('keydown.memberlist', handleListKeydown) .merge(list); @@ -392,6 +398,45 @@ export function uiSectionRawMemberEditor(context) { } + function handleListKeydown(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + + // Only handle navigation keys when list is focused + if (!['PageDown', 'PageUp', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(event.key)) { + return; + } + + var listNode = d3_select(this).node(); + if (!listNode) return; + + var listHeight = listNode.clientHeight; + var scrollTop = listNode.scrollTop; + var scrollHeight = listNode.scrollHeight; + + switch (event.key) { + case 'PageDown': + listNode.scrollTop = Math.min(scrollTop + listHeight, scrollHeight - listHeight); + break; + case 'PageUp': + listNode.scrollTop = Math.max(scrollTop - listHeight, 0); + break; + case 'End': + listNode.scrollTop = scrollHeight; + break; + case 'Home': + listNode.scrollTop = 0; + break; + case 'ArrowDown': + listNode.scrollTop = Math.min(scrollTop + 40, scrollHeight - listHeight); + break; + case 'ArrowUp': + listNode.scrollTop = Math.max(scrollTop - 40, 0); + break; + } + } + + function unbind() { var row = d3_select(this); diff --git a/modules/ui/sections/raw_membership_editor.js b/modules/ui/sections/raw_membership_editor.js index 4efda067c26..a0c211a73e4 100644 --- a/modules/ui/sections/raw_membership_editor.js +++ b/modules/ui/sections/raw_membership_editor.js @@ -365,6 +365,12 @@ export function uiSectionRawMembershipEditor(context) { list = list.enter() .append('ul') .attr('class', 'member-list') + .attr('tabindex', '0') + .on('mousedown.memberlist', function(event) { + this.focus(); + event.stopPropagation(); + }) + .on('keydown.memberlist', handleListKeydown) .merge(list); @@ -659,6 +665,45 @@ export function uiSectionRawMembershipEditor(context) { } + function handleListKeydown(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + + // Only handle navigation keys when list is focused + if (!['PageDown', 'PageUp', 'End', 'Home', 'ArrowDown', 'ArrowUp'].includes(event.key)) { + return; + } + + var listNode = d3_select(this).node(); + if (!listNode) return; + + var listHeight = listNode.clientHeight; + var scrollTop = listNode.scrollTop; + var scrollHeight = listNode.scrollHeight; + + switch (event.key) { + case 'PageDown': + listNode.scrollTop = Math.min(scrollTop + listHeight, scrollHeight - listHeight); + break; + case 'PageUp': + listNode.scrollTop = Math.max(scrollTop - listHeight, 0); + break; + case 'End': + listNode.scrollTop = scrollHeight; + break; + case 'Home': + listNode.scrollTop = 0; + break; + case 'ArrowDown': + listNode.scrollTop = Math.min(scrollTop + 40, scrollHeight - listHeight); + break; + case 'ArrowUp': + listNode.scrollTop = Math.max(scrollTop - 40, 0); + break; + } + } + + function unbind() { var row = d3_select(this);