2222 *
2323 ******************************************************************************/
2424
25- import React , { MouseEvent , useContext , useState , useEffect , useRef } from "react"
25+ import React , { MouseEvent , useState , useEffect , useRef } from "react"
2626import styled from "styled-components"
2727import { Rocket , InfoCircle } from "@styled-icons/boxicons-regular"
2828import { SortDown } from "@styled-icons/boxicons-regular"
@@ -38,11 +38,12 @@ import { TableIcon } from "../table-icon"
3838import { Box } from "@questdb/react-components"
3939import { Text , TransitionDuration , IconWithTooltip , spinAnimation } from "../../../components"
4040import { color } from "../../../utils"
41- import { SchemaContext } from "../SchemaContext"
41+ import { useSchema } from "../SchemaContext"
4242import { Checkbox } from "../checkbox"
4343import { PopperHover } from "../../../components/PopperHover"
4444import { Tooltip } from "../../../components/Tooltip"
4545import { mapColumnTypeToUI } from "../../../scenes/Import/ImportCSVFiles/utils"
46+ import { MATVIEWS_GROUP_KEY } from "../localStorageUtils"
4647
4748type Props = Readonly < {
4849 className ?: string
@@ -60,10 +61,11 @@ type Props = Readonly<{
6061 selectOpen ?: boolean
6162 selected ?: boolean
6263 onSelectToggle ?: ( { name, type} : { name : string , type : TreeNodeKind } ) => void
63- baseTable ?: string
6464 errors ?: string [ ]
6565 value ?: string
6666 includesSymbol ?: boolean
67+ path ?: string
68+ tabIndex ?: number
6769} >
6870
6971const Type = styled ( Text ) `
@@ -86,6 +88,8 @@ const Wrapper = styled.div<{ $isExpandable: boolean, $includesSymbol?: boolean }
8688 padding-left: 1rem;
8789 padding-right: 1rem;
8890 user-select: none;
91+ border: 1px solid transparent;
92+ border-radius: 0.4rem;
8993 ${ ( { $isExpandable } ) => $isExpandable && `
9094 cursor: pointer;
9195 ` }
@@ -98,6 +102,12 @@ const Wrapper = styled.div<{ $isExpandable: boolean, $includesSymbol?: boolean }
98102 &:active {
99103 background: ${ color ( "selection" ) } ;
100104 }
105+
106+ &:focus-visible, &.focused {
107+ outline: none;
108+ background: ${ color ( "selection" ) } ;
109+ border: 1px solid ${ color ( "comment" ) } ;
110+ }
101111`
102112
103113const StyledTitle = styled ( Title ) `
@@ -121,6 +131,8 @@ const StyledTitle = styled(Title)`
121131const TableActions = styled . span `
122132 z-index: 1;
123133 position: relative;
134+ display: inline-flex;
135+ align-items: center;
124136`
125137
126138const FlexRow = styled . div < { $selectOpen ?: boolean } > `
@@ -165,7 +177,8 @@ const Loader = styled(Loader4)`
165177`
166178
167179const ErrorIconWrapper = styled . div `
168- display: inline;
180+ display: inline-flex;
181+ align-items: center;
169182 align-self: center;
170183
171184 svg {
@@ -257,6 +270,43 @@ const ColumnIcon = ({
257270 return getIcon ( type )
258271}
259272
273+ export const isElementVisible = ( element : HTMLElement | undefined , container : HTMLElement | null ) => {
274+ if ( ! element || ! container ) return false
275+ const elementRect = element . getBoundingClientRect ( )
276+ const containerRect = container instanceof Window
277+ ? { top : 0 , bottom : window . innerHeight }
278+ : container . getBoundingClientRect ( )
279+
280+ const visibleTop = Math . max ( elementRect . top , containerRect . top )
281+ const visibleBottom = Math . min ( elementRect . bottom , containerRect . bottom )
282+ const visibleHeight = Math . max ( 0 , visibleBottom - visibleTop )
283+
284+ const totalHeight = elementRect . bottom - elementRect . top
285+
286+ return visibleHeight >= totalHeight * 0.5
287+ }
288+
289+ export const computeFocusableElements = ( scrollerRef : HTMLElement ) => {
290+ const allElements = Array . from ( document . querySelectorAll ( '[tabindex="100"], [tabindex="101"], [tabindex="200"], [tabindex="201"]' ) )
291+
292+ const focusableElements = allElements
293+ . filter ( element => isElementVisible ( element as HTMLElement , scrollerRef ) )
294+ . sort ( ( a , b ) => {
295+ const tabIndexA = parseInt ( a . getAttribute ( 'tabindex' ) || '0' )
296+ const tabIndexB = parseInt ( b . getAttribute ( 'tabindex' ) || '0' )
297+
298+ if ( tabIndexA !== tabIndexB ) {
299+ return tabIndexA - tabIndexB
300+ }
301+
302+ const positionA = allElements . indexOf ( a )
303+ const positionB = allElements . indexOf ( b )
304+ return positionA - positionB
305+ } )
306+
307+ return focusableElements
308+ }
309+
260310const Row = ( {
261311 className,
262312 designatedTimestamp,
@@ -273,12 +323,13 @@ const Row = ({
273323 selectOpen,
274324 selected,
275325 onSelectToggle,
276- baseTable,
277326 errors,
278327 value,
279- includesSymbol
328+ includesSymbol,
329+ path,
330+ tabIndex,
280331} : Props ) => {
281- const { query } = useContext ( SchemaContext )
332+ const { query, scrollBy , scrollerRef } = useSchema ( )
282333 const [ showLoader , setShowLoader ] = useState ( false )
283334 const timeoutRef = useRef < NodeJS . Timeout | null > ( null )
284335 const isExpandable = [ "folder" , "table" , "matview" ] . includes ( kind ) || ( kind === "column" && type === "SYMBOL" )
@@ -301,19 +352,91 @@ const Row = ({
301352 }
302353 } , [ isLoading ] )
303354
355+ const getTabIndex = ( ) => {
356+ if ( tabIndex ) {
357+ return tabIndex
358+ }
359+ if ( path ?. startsWith ( MATVIEWS_GROUP_KEY ) ) {
360+ return 201
361+ }
362+ return 101
363+ }
364+
304365 return (
305366 < Wrapper
306367 $isExpandable = { isExpandable }
307368 $includesSymbol = { includesSymbol }
308369 data-hook = { dataHook ?? "schema-row" }
370+ data-kind = { kind }
371+ data-path = { path }
309372 className = { className }
373+ tabIndex = { getTabIndex ( ) }
374+ onFocus = { ( e ) => {
375+ ; ( e . target as HTMLElement ) . classList . add ( 'focused' )
376+ } }
377+ onBlur = { ( e ) => {
378+ ; ( e . target as HTMLElement ) . classList . remove ( 'focused' )
379+ } }
310380 onClick = { ( e ) => {
381+ const target = e . target as HTMLElement
382+ target . focus ( ) ;
311383 if ( isTableKind && selectOpen && onSelectToggle ) {
312384 onSelectToggle ( { name, type : kind } )
313385 } else {
314386 onClick ?.( e )
315387 }
316388 } }
389+ onKeyDown = { ( e ) => {
390+ if ( ! path ) return
391+ if ( ! scrollerRef . current || ! isElementVisible ( document . activeElement as HTMLElement , scrollerRef . current ) ) return
392+ if ( isExpandable ) {
393+ if (
394+ e . key === "Enter"
395+ || ( e . key === "ArrowRight" && ! expanded )
396+ || ( e . key === "ArrowLeft" && expanded )
397+ ) {
398+ // @ts -ignore
399+ onClick ?.( )
400+ }
401+ }
402+ if ( e . key === "ArrowDown" ) {
403+ e . preventDefault ( )
404+ const currentElement = document . activeElement as HTMLElement
405+ if ( ! currentElement || ! scrollerRef . current ) return
406+ let focusableElements = computeFocusableElements ( scrollerRef . current )
407+ let currentIndex = focusableElements . indexOf ( currentElement )
408+
409+ if ( currentIndex === focusableElements . length - 1 ) {
410+ scrollBy ( 32 )
411+ }
412+ focusableElements = computeFocusableElements ( scrollerRef . current )
413+ currentIndex = focusableElements . indexOf ( document . activeElement as HTMLElement )
414+
415+ if ( currentIndex < focusableElements . length - 1 ) {
416+ const nextElement = focusableElements [ currentIndex + 1 ] as HTMLElement
417+ nextElement . focus ( )
418+ }
419+ }
420+ if ( e . key === "ArrowUp" ) {
421+ e . preventDefault ( )
422+ if ( ! document . activeElement || ! scrollerRef . current ) return
423+ let focusableElements = computeFocusableElements ( scrollerRef . current )
424+ let currentIndex = focusableElements . indexOf ( document . activeElement as HTMLElement )
425+
426+ if ( currentIndex === 0 ) {
427+ scrollBy ( - 32 )
428+ }
429+ setTimeout ( ( ) => {
430+ focusableElements = computeFocusableElements ( scrollerRef . current ! )
431+ currentIndex = focusableElements . indexOf ( document . activeElement as HTMLElement )
432+ if ( currentIndex > 0 ) {
433+ const previousElement = focusableElements [ currentIndex - 1 ] as HTMLElement
434+ previousElement . focus ( )
435+ }
436+ } , 0 )
437+
438+ }
439+ } }
317440 >
318441 < Box
319442 align = "center"
0 commit comments