11import React from 'react' ;
22
3- import { Button , Flex , PortalProvider } from '@gravity-ui/uikit' ;
3+ import { ChevronUp } from '@gravity-ui/icons' ;
4+ import { Button , Flex , Icon , PortalProvider } from '@gravity-ui/uikit' ;
45import { createPortal } from 'react-dom' ;
56
6- import { getDiskStateColorMods } from '../../../components/DiskStateProgressBar/DiskStateProgressBar' ;
77import { SETTING_KEYS } from '../../../store/reducers/settings/constants' ;
88import { cn } from '../../../utils/cn' ;
9- import {
10- getDefaultDiskDisplayState ,
11- getDefaultPDiskDisplayState ,
12- } from '../../../utils/disks/displayState' ;
13- import { getDiskBarTone } from '../../../utils/disks/getDiskBarTone' ;
14- import type { DiskBarTone , PreparedPDisk , PreparedVDisk } from '../../../utils/disks/types' ;
9+ import { DONOR_COLOR } from '../../../utils/disks/constants' ;
10+ import { getDataSeverityColor } from '../../../utils/disks/helpers' ;
11+ import type { PreparedPDisk , PreparedVDisk } from '../../../utils/disks/types' ;
1512import { useSetting } from '../../../utils/hooks/useSetting' ;
1613import { isNumeric } from '../../../utils/utils' ;
1714import { PDisk } from '../PDisk' ;
@@ -62,13 +59,16 @@ interface PDiskPreviewItemProps extends PDiskPreviewSvgProps {
6259 id : string ;
6360 expandedWidth : number ;
6461 highlightedDisk ?: string ;
65- setHighlightedDisk : ( id ?: string ) => void ;
62+ setHighlightedDisk : React . Dispatch < React . SetStateAction < string | undefined > > ;
6663}
6764
65+ // The preview uses ordinary prepared-data severity, independently of Expert mode display states.
66+ type PreviewTone = ReturnType < typeof getDataSeverityColor > | typeof DONOR_COLOR ;
67+
6868interface VDiskPath {
6969 key : string ;
7070 path : string ;
71- tone : DiskBarTone ;
71+ tone : PreviewTone ;
7272 inactive : boolean ;
7373}
7474
@@ -86,8 +86,8 @@ function getFilledSize(percent: unknown, size: number, inverted = false) {
8686 return ( filledPercent / 100 ) * size ;
8787}
8888
89- function getBarClassName ( element : string , tone : DiskBarTone , highlighted ?: boolean ) {
90- return b ( 'color' , getDiskStateColorMods ( tone , highlighted ) , b ( element ) ) ;
89+ function getBarClassName ( element : string , tone : PreviewTone , highlighted ?: boolean ) {
90+ return b ( 'color' , { [ tone . toLowerCase ( ) ] : true , highlighted} , b ( element ) ) ;
9191}
9292
9393function getPDiskKey ( pDisk : PreparedPDisk , index : number ) {
@@ -102,10 +102,7 @@ function getVDiskPaths(vDisks: PreparedVDisk[], viewContext?: StorageViewContext
102102 const row = index % VDISKS_WITHOUT_PDISK_COLUMN ;
103103 const x = VDISK_X + column * ( VDISK_SIZE + VDISK_COLUMN_GAP ) ;
104104 const y = row * ( VDISK_SIZE + VDISK_GAP ) ;
105- const tone = getDiskBarTone ( {
106- severity : getDefaultDiskDisplayState ( vDisk , vDisk . DonorMode ) . severity ,
107- isDonor : vDisk . DonorMode ,
108- } ) ;
105+ const tone = vDisk . DonorMode ? DONOR_COLOR : getDataSeverityColor ( vDisk . Severity ) ;
109106 const inactive = ! isVdiskActive ( vDisk , viewContext ) ;
110107 const key = `${ tone } -${ inactive } ` ;
111108 const rectPath = `M${ x } ${ y } h${ VDISK_SIZE } v${ VDISK_SIZE } h-${ VDISK_SIZE } z` ;
@@ -143,9 +140,8 @@ function PDiskPreviewSvg({
143140 highlighted,
144141 inverted,
145142} : PDiskPreviewSvgProps ) {
146- const displayState = getDefaultPDiskDisplayState ( pDisk ) ;
147- const pDiskTone = getDiskBarTone ( { severity : displayState . severity } ) ;
148- const pDiskFilledHeight = getFilledSize ( displayState . allocatedPercent , PDISK_HEIGHT , inverted ) ;
143+ const pDiskTone = getDataSeverityColor ( pDisk . Severity ) ;
144+ const pDiskFilledHeight = getFilledSize ( pDisk . AllocatedPercent , PDISK_HEIGHT , inverted ) ;
149145 const pDiskFillY = inverted ? PDISK_Y : PDISK_Y + PDISK_HEIGHT - pDiskFilledHeight ;
150146 const vDiskPaths = React . useMemo (
151147 ( ) => getVDiskPaths ( vDisks , viewContext ) ,
@@ -227,14 +223,28 @@ function PDiskPreviewItem({
227223 highlightedDisk,
228224 setHighlightedDisk,
229225} : PDiskPreviewItemProps ) {
226+ const detailsId = React . useId ( ) ;
230227 const [ detailsOpened , setDetailsOpened ] = React . useState ( false ) ;
231228 const [ previewHovered , setPreviewHovered ] = React . useState ( false ) ;
232229 const [ previewFocused , setPreviewFocused ] = React . useState ( false ) ;
233230 const popupContainerRef = React . useRef < HTMLElement | null > ( null ) ;
234231 const detailsRef = React . useRef < HTMLDivElement > ( null ) ;
232+ const diskLinkRef = React . useRef < HTMLAnchorElement > ( null ) ;
233+ const previewRef = React . useRef < HTMLButtonElement > ( null ) ;
234+ const transferFocusRef = React . useRef ( false ) ;
235235 const popupHostRef = React . useRef < HTMLDivElement > ( null ) ;
236236 const [ popupAvailableHeight , setPopupAvailableHeight ] = React . useState ( 0 ) ;
237237
238+ React . useLayoutEffect ( ( ) => {
239+ if ( ! transferFocusRef . current ) {
240+ return ;
241+ }
242+ transferFocusRef . current = false ;
243+ // Transfer keyboard focus after React replaces the activated control.
244+ const target = detailsOpened ? diskLinkRef . current : previewRef . current ;
245+ target ?. focus ( { preventScroll : true } ) ;
246+ } , [ detailsOpened ] ) ;
247+
238248 React . useLayoutEffect ( ( ) => {
239249 const container = popupContainerRef . current ;
240250 const details = detailsRef . current ;
@@ -264,15 +274,26 @@ function PDiskPreviewItem({
264274 window . removeEventListener ( 'resize' , updatePopupHeight ) ;
265275 } ;
266276 } , [ detailsOpened , highlightedDisk ] ) ;
277+ const clearOwnHighlight = React . useCallback ( ( ) => {
278+ setHighlightedDisk ( ( current ) =>
279+ current === id || vDisks . some ( ( disk ) => disk . StringifiedId === current )
280+ ? undefined
281+ : current ,
282+ ) ;
283+ } , [ id , vDisks , setHighlightedDisk ] ) ;
267284 const detailsVersionRef = React . useRef ( 0 ) ;
268285 const detailsVersion = detailsVersionRef . current ;
269286 const setDetailsHighlightedDisk = React . useCallback (
270287 ( diskId ?: string ) => {
271288 if ( detailsVersionRef . current === detailsVersion ) {
272- setHighlightedDisk ( diskId ) ;
289+ if ( diskId === undefined ) {
290+ clearOwnHighlight ( ) ;
291+ } else {
292+ setHighlightedDisk ( diskId ) ;
293+ }
273294 }
274295 } ,
275- [ detailsVersion , setHighlightedDisk ] ,
296+ [ detailsVersion , setHighlightedDisk , clearOwnHighlight ] ,
276297 ) ;
277298
278299 const closeDetails = React . useCallback ( ( ) => {
@@ -281,8 +302,8 @@ function PDiskPreviewItem({
281302 setPreviewHovered ( false ) ;
282303 setPreviewFocused ( false ) ;
283304 setDetailsOpened ( false ) ;
284- setHighlightedDisk ( undefined ) ;
285- } , [ setHighlightedDisk ] ) ;
305+ clearOwnHighlight ( ) ;
306+ } , [ clearOwnHighlight ] ) ;
286307
287308 const handleOpenedDetailsClick = React . useCallback (
288309 ( event : React . MouseEvent < unknown > ) => {
@@ -295,6 +316,7 @@ function PDiskPreviewItem({
295316
296317 event . preventDefault ( ) ;
297318 event . stopPropagation ( ) ;
319+ transferFocusRef . current = event . detail === 0 ;
298320 closeDetails ( ) ;
299321 } ,
300322 [ closeDetails ] ,
@@ -305,12 +327,14 @@ function PDiskPreviewItem({
305327 < Button
306328 view = "flat"
307329 className = { b ( 'control' ) }
330+ ref = { previewRef }
308331 onMouseEnter = { ( ) => setPreviewHovered ( true ) }
309332 onMouseLeave = { ( ) => setPreviewHovered ( false ) }
310333 onFocus = { ( ) => setPreviewFocused ( true ) }
311334 onBlur = { ( ) => setPreviewFocused ( false ) }
312335 onClick = { ( event ) => {
313336 event . stopPropagation ( ) ;
337+ transferFocusRef . current = event . detail === 0 ;
314338 // Keep popup wheel events in the table's native scroll container.
315339 let container = event . currentTarget . parentElement ;
316340 while (
@@ -322,6 +346,7 @@ function PDiskPreviewItem({
322346 popupContainerRef . current = container ;
323347 setDetailsOpened ( true ) ;
324348 } }
349+ aria-expanded = { false }
325350 aria-label = { i18n ( 'action_show-pdisks-details' , { id} ) }
326351 >
327352 < PDiskPreviewSvg
@@ -360,19 +385,30 @@ function PDiskPreviewItem({
360385 ref = { detailsRef }
361386 onClickCapture = { handleOpenedDetailsClick }
362387 >
363- < PDisk
364- data = { pDisk }
365- inactive = { ! isPdiskActive ( pDisk , viewContext ) }
366- vDisks = { vDisks }
367- viewContext = { viewContext }
368- width = { expandedWidth }
369- showPopup = { highlightedDisk === id }
370- onShowPopup = { ( ) => setDetailsHighlightedDisk ( id ) }
371- onHidePopup = { ( ) => setDetailsHighlightedDisk ( undefined ) }
372- highlighted = { highlightedDisk === id }
373- highlightedDisk = { highlightedDisk }
374- setHighlightedDisk = { setDetailsHighlightedDisk }
375- />
388+ < Button
389+ view = "flat"
390+ aria-label = { i18n ( 'action_hide-pdisks-details' , { id} ) }
391+ aria-expanded = { true }
392+ aria-controls = { detailsId }
393+ >
394+ < Icon data = { ChevronUp } size = { 16 } />
395+ </ Button >
396+ < div id = { detailsId } >
397+ < PDisk
398+ linkRef = { diskLinkRef }
399+ data = { pDisk }
400+ inactive = { ! isPdiskActive ( pDisk , viewContext ) }
401+ vDisks = { vDisks }
402+ viewContext = { viewContext }
403+ width = { expandedWidth }
404+ showPopup = { highlightedDisk === id }
405+ onShowPopup = { ( ) => setDetailsHighlightedDisk ( id ) }
406+ onHidePopup = { ( ) => setDetailsHighlightedDisk ( undefined ) }
407+ highlighted = { highlightedDisk === id }
408+ highlightedDisk = { highlightedDisk }
409+ setHighlightedDisk = { setDetailsHighlightedDisk }
410+ />
411+ </ div >
376412 </ Flex >
377413 </ PortalProvider >
378414 </ React . Fragment >
0 commit comments