mirror of https://github.com/grafana/grafana.git
				
				
				
			Draggable: only set drag handle props on the drag handle itself (#50025)
This commit is contained in:
		
							parent
							
								
									752d2286eb
								
							
						
					
					
						commit
						0de9808008
					
				|  | @ -104,7 +104,7 @@ exports[`no enzyme tests`] = { | ||||||
|     "public/app/core/components/QueryOperationRow/QueryOperationAction.test.tsx:3032694716": [ |     "public/app/core/components/QueryOperationRow/QueryOperationAction.test.tsx:3032694716": [ | ||||||
|       [0, 19, 13, "RegExp match", "2409514259"] |       [0, 19, 13, "RegExp match", "2409514259"] | ||||||
|     ], |     ], | ||||||
|     "public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx:2026575657": [ |     "public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx:3743889097": [ | ||||||
|       [0, 26, 13, "RegExp match", "2409514259"] |       [0, 26, 13, "RegExp match", "2409514259"] | ||||||
|     ], |     ], | ||||||
|     "public/app/core/components/Select/MetricSelect.test.tsx:3351544014": [ |     "public/app/core/components/Select/MetricSelect.test.tsx:3351544014": [ | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ describe('QueryOperationRow', () => { | ||||||
|   describe('headerElement rendering', () => { |   describe('headerElement rendering', () => { | ||||||
|     it('should render headerElement provided as element', () => { |     it('should render headerElement provided as element', () => { | ||||||
|       const title = <div aria-label="test title">Test</div>; |       const title = <div aria-label="test title">Test</div>; | ||||||
|       const wrapper = shallow( |       const wrapper = mount( | ||||||
|         <QueryOperationRow headerElement={title} id="test-id" index={0}> |         <QueryOperationRow headerElement={title} id="test-id" index={0}> | ||||||
|           <div>Test</div> |           <div>Test</div> | ||||||
|         </QueryOperationRow> |         </QueryOperationRow> | ||||||
|  | @ -72,7 +72,7 @@ describe('QueryOperationRow', () => { | ||||||
| 
 | 
 | ||||||
|     it('should render headerElement provided as function', () => { |     it('should render headerElement provided as function', () => { | ||||||
|       const title = () => <div aria-label="test title">Test</div>; |       const title = () => <div aria-label="test title">Test</div>; | ||||||
|       const wrapper = shallow( |       const wrapper = mount( | ||||||
|         <QueryOperationRow headerElement={title} id="test-id" index={0}> |         <QueryOperationRow headerElement={title} id="test-id" index={0}> | ||||||
|           <div>Test</div> |           <div>Test</div> | ||||||
|         </QueryOperationRow> |         </QueryOperationRow> | ||||||
|  | @ -101,7 +101,7 @@ describe('QueryOperationRow', () => { | ||||||
|   describe('actions rendering', () => { |   describe('actions rendering', () => { | ||||||
|     it('should render actions provided as element', () => { |     it('should render actions provided as element', () => { | ||||||
|       const actions = <div aria-label="test actions">Test</div>; |       const actions = <div aria-label="test actions">Test</div>; | ||||||
|       const wrapper = shallow( |       const wrapper = mount( | ||||||
|         <QueryOperationRow actions={actions} id="test-id" index={0}> |         <QueryOperationRow actions={actions} id="test-id" index={0}> | ||||||
|           <div>Test</div> |           <div>Test</div> | ||||||
|         </QueryOperationRow> |         </QueryOperationRow> | ||||||
|  | @ -112,7 +112,7 @@ describe('QueryOperationRow', () => { | ||||||
|     }); |     }); | ||||||
|     it('should render actions provided as function', () => { |     it('should render actions provided as function', () => { | ||||||
|       const actions = () => <div aria-label="test actions">Test</div>; |       const actions = () => <div aria-label="test actions">Test</div>; | ||||||
|       const wrapper = shallow( |       const wrapper = mount( | ||||||
|         <QueryOperationRow actions={actions} id="test-id" index={0}> |         <QueryOperationRow actions={actions} id="test-id" index={0}> | ||||||
|           <div>Test</div> |           <div>Test</div> | ||||||
|         </QueryOperationRow> |         </QueryOperationRow> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,13 @@ | ||||||
| import { css, cx } from '@emotion/css'; | import { css } from '@emotion/css'; | ||||||
| import React, { useCallback, useState } from 'react'; | import React, { useCallback, useState } from 'react'; | ||||||
| import { Draggable } from 'react-beautiful-dnd'; | import { Draggable } from 'react-beautiful-dnd'; | ||||||
| import { useUpdateEffect } from 'react-use'; | import { useUpdateEffect } from 'react-use'; | ||||||
| 
 | 
 | ||||||
| import { GrafanaTheme } from '@grafana/data'; | import { GrafanaTheme } from '@grafana/data'; | ||||||
| import { reportInteraction } from '@grafana/runtime'; | import { reportInteraction } from '@grafana/runtime'; | ||||||
| import { Icon, ReactUtils, stylesFactory, useTheme } from '@grafana/ui'; | import { ReactUtils, stylesFactory, useTheme } from '@grafana/ui'; | ||||||
|  | 
 | ||||||
|  | import { QueryOperationRowHeader } from './QueryOperationRowHeader'; | ||||||
| 
 | 
 | ||||||
| interface QueryOperationRowProps { | interface QueryOperationRowProps { | ||||||
|   index: number; |   index: number; | ||||||
|  | @ -93,41 +95,25 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({ | ||||||
|   const actionsElement = actions && ReactUtils.renderOrCallToRender(actions, renderPropArgs); |   const actionsElement = actions && ReactUtils.renderOrCallToRender(actions, renderPropArgs); | ||||||
|   const headerElementRendered = headerElement && ReactUtils.renderOrCallToRender(headerElement, renderPropArgs); |   const headerElementRendered = headerElement && ReactUtils.renderOrCallToRender(headerElement, renderPropArgs); | ||||||
| 
 | 
 | ||||||
|   const rowHeader = ( |  | ||||||
|     <div className={styles.header}> |  | ||||||
|       <div className={styles.column}> |  | ||||||
|         <Icon |  | ||||||
|           name={isContentVisible ? 'angle-down' : 'angle-right'} |  | ||||||
|           className={styles.collapseIcon} |  | ||||||
|           onClick={onRowToggle} |  | ||||||
|         /> |  | ||||||
|         {title && ( |  | ||||||
|           <div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title"> |  | ||||||
|             <div className={cx(styles.title, disabled && styles.disabled)}>{titleElement}</div> |  | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|         {headerElementRendered} |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div className={styles.column}> |  | ||||||
|         {actionsElement} |  | ||||||
|         {draggable && ( |  | ||||||
|           <Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} /> |  | ||||||
|         )} |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   if (draggable) { |   if (draggable) { | ||||||
|     return ( |     return ( | ||||||
|       <Draggable draggableId={id} index={index}> |       <Draggable draggableId={id} index={index}> | ||||||
|         {(provided) => { |         {(provided) => { | ||||||
|           const dragHandleProps = { ...provided.dragHandleProps, role: 'group' }; // replace the role="button" because it causes https://dequeuniversity.com/rules/axe/4.3/nested-interactive?application=msftAI
 |  | ||||||
|           return ( |           return ( | ||||||
|             <> |             <> | ||||||
|               <div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}> |               <div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}> | ||||||
|                 <div {...dragHandleProps} onMouseMove={reportDragMousePosition}> |                 <div> | ||||||
|                   {rowHeader} |                   <QueryOperationRowHeader | ||||||
|  |                     actionsElement={actionsElement} | ||||||
|  |                     disabled={disabled} | ||||||
|  |                     draggable | ||||||
|  |                     dragHandleProps={provided.dragHandleProps} | ||||||
|  |                     headerElement={headerElementRendered} | ||||||
|  |                     isContentVisible={isContentVisible} | ||||||
|  |                     onRowToggle={onRowToggle} | ||||||
|  |                     reportDragMousePosition={reportDragMousePosition} | ||||||
|  |                     titleElement={titleElement} | ||||||
|  |                   /> | ||||||
|                 </div> |                 </div> | ||||||
|                 {isContentVisible && <div className={styles.content}>{children}</div>} |                 {isContentVisible && <div className={styles.content}>{children}</div>} | ||||||
|               </div> |               </div> | ||||||
|  | @ -140,7 +126,16 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({ | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={styles.wrapper}> |     <div className={styles.wrapper}> | ||||||
|       {rowHeader} |       <QueryOperationRowHeader | ||||||
|  |         actionsElement={actionsElement} | ||||||
|  |         disabled={disabled} | ||||||
|  |         draggable={false} | ||||||
|  |         headerElement={headerElementRendered} | ||||||
|  |         isContentVisible={isContentVisible} | ||||||
|  |         onRowToggle={onRowToggle} | ||||||
|  |         reportDragMousePosition={reportDragMousePosition} | ||||||
|  |         titleElement={titleElement} | ||||||
|  |       /> | ||||||
|       {isContentVisible && <div className={styles.content}>{children}</div>} |       {isContentVisible && <div className={styles.content}>{children}</div>} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  | @ -151,63 +146,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => { | ||||||
|     wrapper: css` |     wrapper: css` | ||||||
|       margin-bottom: ${theme.spacing.md}; |       margin-bottom: ${theme.spacing.md}; | ||||||
|     `,
 |     `,
 | ||||||
|     header: css` |  | ||||||
|       label: Header; |  | ||||||
|       padding: ${theme.spacing.xs} ${theme.spacing.sm}; |  | ||||||
|       border-radius: ${theme.border.radius.sm}; |  | ||||||
|       background: ${theme.colors.bg2}; |  | ||||||
|       min-height: ${theme.spacing.formInputHeight}px; |  | ||||||
|       display: grid; |  | ||||||
|       grid-template-columns: minmax(100px, max-content) min-content; |  | ||||||
|       align-items: center; |  | ||||||
|       justify-content: space-between; |  | ||||||
|       white-space: nowrap; |  | ||||||
| 
 |  | ||||||
|       &:focus { |  | ||||||
|         outline: none; |  | ||||||
|       } |  | ||||||
|     `,
 |  | ||||||
|     column: css` |  | ||||||
|       label: Column; |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|     `,
 |  | ||||||
|     dragIcon: css` |  | ||||||
|       cursor: grab; |  | ||||||
|       color: ${theme.colors.textWeak}; |  | ||||||
|       &:hover { |  | ||||||
|         color: ${theme.colors.text}; |  | ||||||
|       } |  | ||||||
|     `,
 |  | ||||||
|     collapseIcon: css` |  | ||||||
|       color: ${theme.colors.textWeak}; |  | ||||||
|       cursor: pointer; |  | ||||||
|       &:hover { |  | ||||||
|         color: ${theme.colors.text}; |  | ||||||
|       } |  | ||||||
|     `,
 |  | ||||||
|     titleWrapper: css` |  | ||||||
|       display: flex; |  | ||||||
|       align-items: center; |  | ||||||
|       flex-grow: 1; |  | ||||||
|       cursor: pointer; |  | ||||||
|       overflow: hidden; |  | ||||||
|       margin-right: ${theme.spacing.sm}; |  | ||||||
|     `,
 |  | ||||||
|     title: css` |  | ||||||
|       font-weight: ${theme.typography.weight.semibold}; |  | ||||||
|       color: ${theme.colors.textBlue}; |  | ||||||
|       margin-left: ${theme.spacing.sm}; |  | ||||||
|       overflow: hidden; |  | ||||||
|       text-overflow: ellipsis; |  | ||||||
|     `,
 |  | ||||||
|     content: css` |     content: css` | ||||||
|       margin-top: ${theme.spacing.inlineFormMargin}; |       margin-top: ${theme.spacing.inlineFormMargin}; | ||||||
|       margin-left: ${theme.spacing.lg}; |       margin-left: ${theme.spacing.lg}; | ||||||
|     `,
 |     `,
 | ||||||
|     disabled: css` |  | ||||||
|       color: ${theme.colors.textWeak}; |  | ||||||
|     `,
 |  | ||||||
|   }; |   }; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,123 @@ | ||||||
|  | import { css, cx } from '@emotion/css'; | ||||||
|  | import React, { MouseEventHandler } from 'react'; | ||||||
|  | import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; | ||||||
|  | 
 | ||||||
|  | import { GrafanaTheme2 } from '@grafana/data'; | ||||||
|  | import { Icon, useStyles2 } from '@grafana/ui'; | ||||||
|  | 
 | ||||||
|  | interface QueryOperationRowHeaderProps { | ||||||
|  |   actionsElement?: React.ReactNode; | ||||||
|  |   disabled?: boolean; | ||||||
|  |   draggable: boolean; | ||||||
|  |   dragHandleProps?: DraggableProvidedDragHandleProps; | ||||||
|  |   headerElement?: React.ReactNode; | ||||||
|  |   isContentVisible: boolean; | ||||||
|  |   onRowToggle: () => void; | ||||||
|  |   reportDragMousePosition: MouseEventHandler<HTMLDivElement>; | ||||||
|  |   titleElement?: React.ReactNode; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const QueryOperationRowHeader: React.FC<QueryOperationRowHeaderProps> = ({ | ||||||
|  |   actionsElement, | ||||||
|  |   disabled, | ||||||
|  |   draggable, | ||||||
|  |   dragHandleProps, | ||||||
|  |   headerElement, | ||||||
|  |   isContentVisible, | ||||||
|  |   onRowToggle, | ||||||
|  |   reportDragMousePosition, | ||||||
|  |   titleElement, | ||||||
|  | }: QueryOperationRowHeaderProps) => { | ||||||
|  |   const styles = useStyles2(getStyles); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className={styles.header}> | ||||||
|  |       <div className={styles.column}> | ||||||
|  |         <Icon | ||||||
|  |           name={isContentVisible ? 'angle-down' : 'angle-right'} | ||||||
|  |           className={styles.collapseIcon} | ||||||
|  |           onClick={onRowToggle} | ||||||
|  |         /> | ||||||
|  |         {titleElement && ( | ||||||
|  |           <div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title"> | ||||||
|  |             <div className={cx(styles.title, disabled && styles.disabled)}>{titleElement}</div> | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  |         {headerElement} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div className={styles.column}> | ||||||
|  |         {actionsElement} | ||||||
|  |         {draggable && ( | ||||||
|  |           <Icon | ||||||
|  |             title="Drag and drop to reorder" | ||||||
|  |             name="draggabledots" | ||||||
|  |             size="lg" | ||||||
|  |             className={styles.dragIcon} | ||||||
|  |             onMouseMove={reportDragMousePosition} | ||||||
|  |             {...dragHandleProps} | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const getStyles = (theme: GrafanaTheme2) => ({ | ||||||
|  |   header: css` | ||||||
|  |     label: Header; | ||||||
|  |     padding: ${theme.spacing(0.5, 0.5)}; | ||||||
|  |     border-radius: ${theme.shape.borderRadius(1)}; | ||||||
|  |     background: ${theme.colors.background.secondary}; | ||||||
|  |     min-height: ${theme.spacing(4)}; | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: minmax(100px, max-content) min-content; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     white-space: nowrap; | ||||||
|  | 
 | ||||||
|  |     &:focus { | ||||||
|  |       outline: none; | ||||||
|  |     } | ||||||
|  |   `,
 | ||||||
|  |   column: css` | ||||||
|  |     label: Column; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |   `,
 | ||||||
|  |   dragIcon: css` | ||||||
|  |     cursor: grab; | ||||||
|  |     color: ${theme.colors.text.disabled}; | ||||||
|  |     margin: ${theme.spacing(0, 0.5)}; | ||||||
|  |     &:hover { | ||||||
|  |       color: ${theme.colors.text}; | ||||||
|  |     } | ||||||
|  |   `,
 | ||||||
|  |   collapseIcon: css` | ||||||
|  |     color: ${theme.colors.text.disabled}; | ||||||
|  |     cursor: pointer; | ||||||
|  |     &:hover { | ||||||
|  |       color: ${theme.colors.text}; | ||||||
|  |     } | ||||||
|  |   `,
 | ||||||
|  |   titleWrapper: css` | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     flex-grow: 1; | ||||||
|  |     cursor: pointer; | ||||||
|  |     overflow: hidden; | ||||||
|  |     margin-right: ${theme.spacing(0.5)}; | ||||||
|  |   `,
 | ||||||
|  |   title: css` | ||||||
|  |     font-weight: ${theme.typography.fontWeightBold}; | ||||||
|  |     color: ${theme.colors.text.link}; | ||||||
|  |     margin-left: ${theme.spacing(0.5)}; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |   `,
 | ||||||
|  |   disabled: css` | ||||||
|  |     color: ${theme.colors.text.disabled}; | ||||||
|  |   `,
 | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | QueryOperationRowHeader.displayName = 'QueryOperationRowHeader'; | ||||||
|  | @ -131,15 +131,16 @@ const DraggableFieldName: React.FC<DraggableFieldProps> = ({ | ||||||
|   return ( |   return ( | ||||||
|     <Draggable draggableId={fieldName} index={index}> |     <Draggable draggableId={fieldName} index={index}> | ||||||
|       {(provided) => ( |       {(provided) => ( | ||||||
|         <div |         <div className="gf-form-inline" ref={provided.innerRef} {...provided.draggableProps}> | ||||||
|           className="gf-form-inline" |  | ||||||
|           ref={provided.innerRef} |  | ||||||
|           {...provided.draggableProps} |  | ||||||
|           {...provided.dragHandleProps} |  | ||||||
|         > |  | ||||||
|           <div className="gf-form gf-form--grow"> |           <div className="gf-form gf-form--grow"> | ||||||
|             <div className="gf-form-label gf-form-label--justify-left width-30"> |             <div className="gf-form-label gf-form-label--justify-left width-30"> | ||||||
|               <Icon name="draggabledots" title="Drag and drop to reorder" size="lg" className={styles.draggable} /> |               <Icon | ||||||
|  |                 name="draggabledots" | ||||||
|  |                 title="Drag and drop to reorder" | ||||||
|  |                 size="lg" | ||||||
|  |                 className={styles.draggable} | ||||||
|  |                 {...provided.dragHandleProps} | ||||||
|  |               /> | ||||||
|               <IconButton |               <IconButton | ||||||
|                 className={styles.toggle} |                 className={styles.toggle} | ||||||
|                 size="md" |                 size="md" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue