11import * as _ from 'lodash' ;
22import * as React from 'react' ;
33import { inject , observer , Observer } from 'mobx-react' ;
4- import { action , computed } from 'mobx' ;
4+ import { action } from 'mobx' ;
55
66import AutoSizer from 'react-virtualized-auto-sizer' ;
77import { FixedSizeList as List , ListChildComponentProps } from 'react-window' ;
@@ -63,7 +63,7 @@ interface ViewEventListProps {
6363 onSelected : ( event : CollectedEvent | undefined ) => void ;
6464}
6565
66- const ListContainer = styled . div < { role : 'table ' } > `
66+ const ListContainer = styled . div < { role : 'grid ' } > `
6767 display: block;
6868 flex-grow: 1;
6969 position: relative;
@@ -83,12 +83,24 @@ const ListContainer = styled.div<{ role: 'table' }>`
8383 pointer-events: none;
8484 }
8585
86+ /* Disable default outline */
8687 & > div > div[tabindex="0"]:focus {
88+ outline: none;
89+ }
90+
91+ /* When focus-visible (keyboard interaction) and no focus is shown on a
92+ row within (activedescendant) - show focus on the list instead */
93+ & > div > div[tabindex="0"]:focus-visible:not([aria-activedescendant]) {
94+ outline: thin dotted ${ p => p . theme . popColor } ;
95+ }
96+
97+ /* When the list is focused, highlight the active row */
98+ & > div > div[tabindex="0"]:focus [aria-selected="true"] {
8799 outline: thin dotted ${ p => p . theme . popColor } ;
88100 }
89101` ;
90102
91- const Column = styled . div < { role : 'cell ' | 'columnheader' } > `
103+ const Column = styled . div < { role : 'gridcell ' | 'columnheader' } > `
92104 display: block;
93105 overflow: hidden;
94106 text-overflow: ellipsis;
@@ -256,10 +268,6 @@ const EventListRow = styled.div<{ role: 'row' }>`
256268 fill: ${ p => p . theme . highlightColor } ;
257269 }
258270 }
259-
260- &:focus {
261- outline: thin dotted ${ p => p . theme . popColor } ;
262- }
263271` ;
264272
265273const TrafficEventListRow = styled ( EventListRow ) `
@@ -439,16 +447,17 @@ const ExchangeRow = inject('uiStore')(observer(({
439447 } `
440448 }
441449 aria-rowindex = { index + 1 }
450+ id = { `event-row-${ exchange . id } ` }
442451 data-event-id = { exchange . id }
443- tabIndex = { isSelected ? 0 : - 1 }
452+ aria-selected = { isSelected }
444453 onContextMenu = { contextMenuBuilder . getContextMenuCallback ( exchange ) }
445454 className = { isSelected ? 'selected' : '' }
446455 style = { style }
447456 >
448457 < RowPin aria-label = { pinned ? 'Pinned' : undefined } pinned = { pinned } />
449- < RowMarker role = 'cell ' category = { category } title = { describeEventCategory ( category ) } />
450- < Method role = 'cell ' pinned = { pinned } > { request . method } </ Method >
451- < Status role = 'cell ' >
458+ < RowMarker role = 'gridcell ' category = { category } title = { describeEventCategory ( category ) } />
459+ < Method role = 'gridcell ' pinned = { pinned } > { request . method } </ Method >
460+ < Status role = 'gridcell ' >
452461 {
453462 response === 'aborted'
454463 ? < StatusCode status = { 'aborted' } />
@@ -471,7 +480,7 @@ const ExchangeRow = inject('uiStore')(observer(({
471480 />
472481 }
473482 </ Status >
474- < Source role = 'cell ' >
483+ < Source role = 'gridcell ' >
475484 < Icon
476485 title = { request . source . summary }
477486 { ...request . source . icon }
@@ -492,10 +501,10 @@ const ExchangeRow = inject('uiStore')(observer(({
492501 />
493502 }
494503 </ Source >
495- < Host role = 'cell ' title = { request . parsedUrl . host } >
504+ < Host role = 'gridcell ' title = { request . parsedUrl . host } >
496505 { request . parsedUrl . host }
497506 </ Host >
498- < PathAndQuery role = 'cell ' title = { request . parsedUrl . pathname + request . parsedUrl . search } >
507+ < PathAndQuery role = 'gridcell ' title = { request . parsedUrl . pathname + request . parsedUrl . search } >
499508 { request . parsedUrl . pathname + request . parsedUrl . search }
500509 </ PathAndQuery >
501510 </ TrafficEventListRow > ;
@@ -537,24 +546,25 @@ const RTCConnectionRow = observer(({
537546 } `
538547 }
539548 aria-rowindex = { index + 1 }
549+ id = { `event-row-${ event . id } ` }
540550 data-event-id = { event . id }
541- tabIndex = { isSelected ? 0 : - 1 }
551+ aria-selected = { isSelected }
542552
543553 className = { isSelected ? 'selected' : '' }
544554 style = { style }
545555 >
546556 < RowPin pinned = { pinned } />
547- < RowMarker role = 'cell ' category = { category } title = { describeEventCategory ( category ) } />
548- < EventTypeColumn role = 'cell ' >
557+ < RowMarker role = 'gridcell ' category = { category } title = { describeEventCategory ( category ) } />
558+ < EventTypeColumn role = 'gridcell ' >
549559 { ! event . closeState && < ConnectedSpinnerIcon /> } WebRTC
550560 </ EventTypeColumn >
551- < Source role = 'cell ' title = { event . source . summary } >
561+ < Source role = 'gridcell ' title = { event . source . summary } >
552562 < Icon
553563 { ...event . source . icon }
554564 fixedWidth = { true }
555565 />
556566 </ Source >
557- < RTCConnectionDetails role = 'cell ' >
567+ < RTCConnectionDetails role = 'gridcell ' >
558568 {
559569 event . clientURL
560570 } < ArrowIcon direction = 'right' /> {
@@ -605,32 +615,33 @@ const RTCStreamRow = observer(({
605615 } `
606616 }
607617 aria-rowindex = { index + 1 }
618+ id = { `event-row-${ event . id } ` }
608619 data-event-id = { event . id }
609- tabIndex = { isSelected ? 0 : - 1 }
620+ aria-selected = { isSelected }
610621
611622 className = { isSelected ? 'selected' : '' }
612623 style = { style }
613624 >
614625 < RowPin pinned = { pinned } />
615- < RowMarker role = 'cell ' category = { category } title = { describeEventCategory ( category ) } />
616- < EventTypeColumn role = 'cell ' >
626+ < RowMarker role = 'gridcell ' category = { category } title = { describeEventCategory ( category ) } />
627+ < EventTypeColumn role = 'gridcell ' >
617628 { ! event . closeState && < ConnectedSpinnerIcon /> } WebRTC {
618629 event . isRTCDataChannel ( )
619630 ? 'Data'
620631 : // RTCMediaTrack:
621632 'Media'
622633 }
623634 </ EventTypeColumn >
624- < Source role = 'cell ' title = { event . rtcConnection . source . summary } >
635+ < Source role = 'gridcell ' title = { event . rtcConnection . source . summary } >
625636 < Icon
626637 { ...event . rtcConnection . source . icon }
627638 fixedWidth = { true }
628639 />
629640 </ Source >
630- < RTCEventLabel role = 'cell ' >
641+ < RTCEventLabel role = 'gridcell ' >
631642 < ArrowIcon direction = 'right' /> { event . rtcConnection . remoteURL }
632643 </ RTCEventLabel >
633- < RTCEventDetails role = 'cell ' >
644+ < RTCEventDetails role = 'gridcell ' >
634645 {
635646 event . isRTCDataChannel ( )
636647 ? < >
@@ -694,25 +705,26 @@ const BuiltInApiRow = observer((p: {
694705 } `
695706 }
696707 aria-rowindex = { p . index + 1 }
708+ id = { `event-row-${ p . exchange . id } ` }
697709 data-event-id = { p . exchange . id }
698- tabIndex = { p . isSelected ? 0 : - 1 }
710+ aria-selected = { p . isSelected }
699711
700712 onContextMenu = { p . contextMenuBuilder . getContextMenuCallback ( p . exchange ) }
701713 className = { p . isSelected ? 'selected' : '' }
702714 style = { p . style }
703715 >
704716 < RowPin pinned = { pinned } />
705- < RowMarker role = 'cell ' category = { category } title = { describeEventCategory ( category ) } />
706- < EventTypeColumn role = 'cell ' >
717+ < RowMarker role = 'gridcell ' category = { category } title = { describeEventCategory ( category ) } />
718+ < EventTypeColumn role = 'gridcell ' >
707719 { api . service . shortName } : { apiOperationName }
708720 </ EventTypeColumn >
709- < Source role = 'cell ' title = { request . source . summary } >
721+ < Source role = 'gridcell ' title = { request . source . summary } >
710722 < Icon
711723 { ...request . source . icon }
712724 fixedWidth = { true }
713725 />
714726 </ Source >
715- < BuiltInApiRequestDetails role = 'cell ' >
727+ < BuiltInApiRequestDetails role = 'gridcell ' >
716728 { apiRequestDescription }
717729 </ BuiltInApiRequestDetails >
718730 </ TrafficEventListRow >
@@ -734,8 +746,9 @@ const RawTunnelRow = observer((p: {
734746 role = "row"
735747 aria-label = { `Non-HTTP connection to ${ connectionTarget } ` }
736748 aria-rowindex = { p . index + 1 }
749+ id = { `event-row-${ event . id } ` }
737750 data-event-id = { event . id }
738- tabIndex = { p . isSelected ? 0 : - 1 }
751+ aria-selected = { p . isSelected }
739752
740753 className = { p . isSelected ? 'selected' : '' }
741754 style = { p . style }
@@ -771,8 +784,9 @@ const TlsRow = observer((p: {
771784 role = "row"
772785 aria-label = { `${ description } connection to ${ connectionTarget } ` }
773786 aria-rowindex = { p . index + 1 }
787+ id = { `event-row-${ tlsEvent . id } ` }
774788 data-event-id = { tlsEvent . id }
775- tabIndex = { p . isSelected ? 0 : - 1 }
789+ aria-selected = { p . isSelected }
776790
777791 className = { p . isSelected ? 'selected' : '' }
778792 style = { p . style }
@@ -790,14 +804,13 @@ const TlsRow = observer((p: {
790804@observer
791805export class ViewEventList extends React . Component < ViewEventListProps > {
792806
793- @computed
794807 get selectedEventId ( ) {
795808 return this . props . selectedEvent
796809 ? this . props . selectedEvent . id
797810 : undefined ;
798811 }
799812
800- @ computed get listItemData ( ) : EventRowProps [ 'data' ] {
813+ get listItemData ( ) : EventRowProps [ 'data' ] {
801814 return {
802815 selectedEvent : this . props . selectedEvent ,
803816 events : this . props . filteredEvents ,
@@ -819,25 +832,36 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
819832 }
820833 } ;
821834
835+ private get activeRowDomId ( ) : string | undefined {
836+ const id = this . selectedEventId ;
837+ if ( ! id ) return undefined ;
838+
839+ const listBody = this . listBodyRef . current ;
840+ if ( ! listBody ) return undefined ;
841+ if ( ! listBody . querySelector ( `[data-event-id='${ id } ']` ) ) return undefined ;
842+
843+ return `event-row-${ id } ` ;
844+ }
845+
822846 private KeyBoundListWindow = observer (
823847 React . forwardRef < HTMLDivElement > (
824848 ( props : any , ref ) => < div
825849 { ...props }
826850 style = { Object . assign ( { } , props . style , { 'overflowY' : 'scroll' } ) }
827851 ref = { ref }
828852
829- onFocus = { this . focusSelectedEvent }
830853 onKeyDown = { this . onKeyDown }
831854 onMouseDown = { this . onListMouseDown }
832- tabIndex = { this . isSelectedEventVisible ( ) ? - 1 : 0 }
855+ aria-activedescendant = { this . activeRowDomId }
856+ tabIndex = { 0 }
833857 />
834858 )
835859 ) ;
836860
837861 render ( ) {
838862 const { events, filteredEvents, isPaused } = this . props ;
839863
840- return < ListContainer role = "table " >
864+ return < ListContainer role = "grid " >
841865 < TableHeaderRow role = "row" >
842866 < MarkerHeader role = "columnheader" aria-label = "Category" />
843867 < Method role = "columnheader" > Method</ Method >
@@ -888,41 +912,16 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
888912 </ ListContainer > ;
889913 }
890914
891- private isSelectedEventVisible = ( ) => {
892- if ( ! this . selectedEventId ) return false ;
893-
894- const listBody = this . listBodyRef . current ;
895- if ( ! listBody ) return false ;
896-
897- return ! ! listBody . querySelector ( `[data-event-id='${ this . selectedEventId } ']` ) ;
898- }
899-
900- private focusEvent ( event ?: CollectedEvent ) {
901- const listBody = this . listBodyRef . current ;
902- if ( ! listBody ) return ;
903-
904- if ( event ) {
905- const rowElement = listBody . querySelector (
906- `[data-event-id='${ event . id } ']`
907- ) as HTMLDivElement ;
908- rowElement ?. focus ( ) ;
909- } else {
910- const listWindow = listBody . parentElement ! ;
911- listWindow . focus ( ) ;
912- }
913- }
914-
915- private focusSelectedEvent = ( ) => {
916- this . focusEvent ( this . props . selectedEvent ) ;
915+ focusListWindow ( ) {
916+ const listWindow = this . listBodyRef . current ?. parentElement ;
917+ listWindow ?. focus ( ) ;
917918 }
918919
919920 focusList ( ) {
921+ this . focusListWindow ( ) ;
920922 const { selectedEvent } = this . props ;
921923 if ( selectedEvent ) {
922924 this . scrollToEvent ( selectedEvent ) ;
923- } else {
924- const listWindow = this . listBodyRef . current ?. parentElement ;
925- listWindow ?. focus ( ) ;
926925 }
927926 }
928927
@@ -954,21 +953,11 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
954953
955954 private hasRestoredScrollState = false ;
956955
957- componentDidUpdate ( prevProps : ViewEventListProps ) {
958- if ( this . listBodyRef . current ?. parentElement ?. contains ( document . activeElement ) ) {
959- // If we previously had something here focused, and we've updated, update
960- // the focus too, to make sure it's in the right place.
961- this . focusSelectedEvent ( ) ;
962- }
963-
956+ componentDidUpdate ( ) {
964957 // If we previously were scrolled to the bottom of the list, but now we're not,
965958 // scroll there again ourselves now.
966959 if ( this . wasListAtBottom && ! this . isListAtBottom ( ) ) {
967960 this . listRef . current ?. scrollToItem ( this . props . events . length - 1 ) ;
968- } else if ( prevProps . selectedEvent !== this . props . selectedEvent && this . props . selectedEvent ) {
969- // If the selected event changed and we have a selected event, scroll to it
970- // This handles restoring the selected event when returning to the tab
971- this . scrollToEvent ( this . props . selectedEvent ) ;
972961 }
973962 }
974963
@@ -992,8 +981,6 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
992981 if ( targetIndex === - 1 ) return ;
993982
994983 this . listRef . current ?. scrollToItem ( targetIndex ) ;
995-
996- requestAnimationFrame ( ( ) => this . focusEvent ( event ) ) ;
997984 }
998985
999986 public scrollToCenterEvent ( event : CollectedEvent ) {
@@ -1016,8 +1003,8 @@ export class ViewEventList extends React.Component<ViewEventListProps> {
10161003 const targetOffset = rowOffset - halfHeight + rowHeight / 2 ;
10171004 list . scrollTo ( _ . clamp ( targetOffset , 0 , maxOffset ) ) ;
10181005
1019- // Focus the row, after the UI has updated, to make it extra obvious:
1020- requestAnimationFrame ( ( ) => this . focusEvent ( event ) ) ;
1006+ // Focus the list so keyboard navigation works from here
1007+ listWindow . focus ( ) ;
10211008 }
10221009
10231010 public scrollToEnd ( ) {
0 commit comments