RadioButton: Make description appear in a Tooltip component (#78010)

This commit is contained in:
Laura Fernández 2023-11-27 13:54:07 +01:00 committed by GitHub
parent 1c270b1dc2
commit e422a92eae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 44 additions and 29 deletions

View File

@ -55,7 +55,7 @@ describe('Exemplars', () => {
cy.contains(dataSourceName).scrollIntoView().should('be.visible').click(); cy.contains(dataSourceName).scrollIntoView().should('be.visible').click();
// Switch to code editor // Switch to code editor
cy.contains('label', 'Code').click(); e2e.components.RadioButton.container().filter(':contains("Code")').click();
// we need to wait for the query-field being lazy-loaded, in two steps: // we need to wait for the query-field being lazy-loaded, in two steps:
// 1. first we wait for the text 'Loading...' to appear // 1. first we wait for the text 'Loading...' to appear

View File

@ -37,7 +37,7 @@ describe('Loki Query Editor', () => {
e2e.components.DataSourcePicker.container().should('be.visible').click(); e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(dataSourceName).scrollIntoView().should('be.visible').click(); cy.contains(dataSourceName).scrollIntoView().should('be.visible').click();
cy.contains('Code').click(); e2e.components.RadioButton.container().filter(':contains("Code")').click();
// Wait for lazy loading // Wait for lazy loading
const monacoLoadingText = 'Loading...'; const monacoLoadingText = 'Loading...';

View File

@ -78,7 +78,8 @@ describe('Loki query builder', () => {
cy.contains(finalQuery).should('be.visible'); cy.contains(finalQuery).should('be.visible');
// Change to code editor // Change to code editor
cy.contains('label', 'Code').click(); e2e.components.RadioButton.container().filter(':contains("Code")').click();
// We need to test this manually because the final query is split into separate DOM elements using cy.contains(finalQuery).should('be.visible'); does not detect the query. // We need to test this manually because the final query is split into separate DOM elements using cy.contains(finalQuery).should('be.visible'); does not detect the query.
cy.contains('rate').should('be.visible'); cy.contains('rate').should('be.visible');
cy.contains('instance1|instance2').should('be.visible'); cy.contains('instance1|instance2').should('be.visible');

View File

@ -35,7 +35,8 @@ describe('MySQL datasource', () => {
}); });
it('code editor autocomplete should handle table name escaping/quoting', () => { it('code editor autocomplete should handle table name escaping/quoting', () => {
cy.get("label[for^='option-code']").should('be.visible').click(); e2e.components.RadioButton.container().filter(':contains("Code")').click();
cy.get('textarea').type('S{downArrow}{enter}'); cy.get('textarea').type('S{downArrow}{enter}');
cy.wait('@tables'); cy.wait('@tables');
cy.get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible'); cy.get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');

View File

@ -12,7 +12,7 @@ describe('Query editor', () => {
cy.contains('gdev-prometheus').scrollIntoView().should('be.visible').click(); cy.contains('gdev-prometheus').scrollIntoView().should('be.visible').click();
const queryText = `rate(http_requests_total{job="grafana"}[5m])`; const queryText = `rate(http_requests_total{job="grafana"}[5m])`;
cy.contains('label', 'Code').click(); e2e.components.RadioButton.container().filter(':contains("Code")').click();
// we need to wait for the query-field being lazy-loaded, in two steps: // we need to wait for the query-field being lazy-loaded, in two steps:
// it is a two-step process: // it is a two-step process:

View File

@ -10,7 +10,7 @@ describe('Visualization suggestions', () => {
// Try visualization suggestions // Try visualization suggestions
e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PanelEditor.toggleVizPicker().click();
cy.contains('Suggestions').click(); e2e.components.RadioButton.container().filter(':contains("Suggestions")').click();
// Verify we see suggestions // Verify we see suggestions
e2e.components.VisualizationPreview.card('Line chart').should('be.visible'); e2e.components.VisualizationPreview.card('Line chart').should('be.visible');

View File

@ -10,6 +10,9 @@
* @alpha * @alpha
*/ */
export const Components = { export const Components = {
RadioButton: {
container: 'data-testid radio-button',
},
Breadcrumbs: { Breadcrumbs: {
breadcrumb: (title: string) => `data-testid ${title} breadcrumb`, breadcrumb: (title: string) => `data-testid ${title} breadcrumb`,
}, },

View File

@ -2,10 +2,11 @@ import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { StringSelector } from '@grafana/e2e-selectors'; import { StringSelector, selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../themes'; import { useStyles2 } from '../../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins'; import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins';
import { Tooltip } from '../../Tooltip/Tooltip';
import { getPropertiesForButtonSize } from '../commonStyles'; import { getPropertiesForButtonSize } from '../commonStyles';
export type RadioButtonSize = 'sm' | 'md'; export type RadioButtonSize = 'sm' | 'md';
@ -43,20 +44,32 @@ export const RadioButton = React.forwardRef<HTMLInputElement, RadioButtonProps>(
) => { ) => {
const styles = useStyles2(getRadioButtonStyles, size, fullWidth); const styles = useStyles2(getRadioButtonStyles, size, fullWidth);
return ( const inputRadioButton = (
<div className={styles.radioOption}> <input
<input type="radio"
type="radio" className={styles.radio}
className={styles.radio} onChange={onChange}
onChange={onChange} onClick={onClick}
onClick={onClick} disabled={disabled}
disabled={disabled} id={id}
id={id} checked={active}
checked={active} name={name}
name={name} aria-label={ariaLabel}
aria-label={ariaLabel || description} ref={ref}
ref={ref} />
/> );
return description ? (
<div className={styles.radioOption} data-testid={selectors.components.RadioButton.container}>
<Tooltip content={description} placement="bottom">
{inputRadioButton}
</Tooltip>
<label className={styles.radioLabel} htmlFor={id} title={description || ariaLabel}>
{children}
</label>
</div>
) : (
<div className={styles.radioOption} data-testid={selectors.components.RadioButton.container}>
{inputRadioButton}
<label className={styles.radioLabel} htmlFor={id} title={description || ariaLabel}> <label className={styles.radioLabel} htmlFor={id} title={description || ariaLabel}>
{children} {children}
</label> </label>
@ -86,15 +99,16 @@ const getRadioButtonStyles = (theme: GrafanaTheme2, size: RadioButtonSize, fullW
radio: css({ radio: css({
position: 'absolute', position: 'absolute',
opacity: 0, opacity: 0,
zIndex: -1000, zIndex: 2,
width: '100% !important', width: '100% !important',
height: '100%', height: '100%',
cursor: 'pointer',
'&:checked + label': { '&:checked + label': {
color: theme.colors.text.primary, color: theme.colors.text.primary,
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
background: theme.colors.action.selected, background: theme.colors.action.selected,
zIndex: 3, zIndex: 1,
}, },
'&:focus + label, &:focus-visible + label': getFocusStyles(theme), '&:focus + label, &:focus-visible + label': getFocusStyles(theme),
@ -119,7 +133,6 @@ const getRadioButtonStyles = (theme: GrafanaTheme2, size: RadioButtonSize, fullW
borderRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
background: theme.colors.background.primary, background: theme.colors.background.primary,
cursor: 'pointer', cursor: 'pointer',
zIndex: 1,
userSelect: 'none', userSelect: 'none',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
flexGrow: 1, flexGrow: 1,

View File

@ -76,7 +76,7 @@ export function RadioButtonGroup<T>({
<div <div
role="radiogroup" role="radiogroup"
aria-label={ariaLabel} aria-label={ariaLabel}
className={cx(styles.radioGroup, fullWidth && styles.fullWidth, className)} className={cx(styles.radioGroup, fullWidth && styles.fullWidth, invalid && styles.invalid, className)}
> >
{options.map((opt, i) => { {options.map((opt, i) => {
const isItemDisabled = disabledOptions && opt.value && disabledOptions.includes(opt.value); const isItemDisabled = disabledOptions && opt.value && disabledOptions.includes(opt.value);

View File

@ -81,15 +81,12 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
[forwardedRef, setTriggerRef] [forwardedRef, setTriggerRef]
); );
// if the child has an aria-label, this should take precedence over the tooltip content
const childHasAriaLabel = 'aria-label' in children.props;
return ( return (
<> <>
{React.cloneElement(children, { {React.cloneElement(children, {
ref: handleRef, ref: handleRef,
tabIndex: 0, // tooltip trigger should be keyboard focusable tabIndex: 0, // tooltip trigger should be keyboard focusable
'aria-describedby': !childHasAriaLabel && visible ? tooltipId : undefined, 'aria-describedby': visible ? tooltipId : undefined,
})} })}
{visible && ( {visible && (
<Portal> <Portal>