From 59067a72b8b254b4bc9512d091a062c1815dbb4c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:56:01 -0400 Subject: [PATCH] Template variables: Keyboard navigation improvements (#38001) (#38074) * Fix variable labels * Add proper labeling for input * Add ids to PickerRenderer * Fix tests * Update PR feedback * OptionsPicker: Change to id * Inherit aria attributes * Add checkbox role * Fix typo * Add proper label reference * Update role and label * Prevent spreadng non-DOM attributes * Move form layout to other component * Remove haspopup * Add testid to selector * Add HTMLProps extension * Use list * Move styles outside of class * Add cx (cherry picked from commit 1f091c448fc85d1248b34af818427b1350860bc9) Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> --- .../src/selectors/pages.ts | 10 ++-- .../components/SubMenu/DashboardLinks.tsx | 6 +-- .../dashboard/components/SubMenu/SubMenu.tsx | 11 +++- .../components/SubMenu/SubMenuItems.tsx | 3 +- .../OptionsPicker/OptionPicker.test.tsx | 4 +- .../pickers/OptionsPicker/OptionsPicker.tsx | 15 +++++- .../variables/pickers/PickerRenderer.tsx | 6 ++- .../pickers/shared/VariableInput.tsx | 7 ++- .../variables/pickers/shared/VariableLink.tsx | 22 +++++--- .../pickers/shared/VariableOptions.tsx | 53 ++++++++++++++----- .../textbox/TextBoxVariablePicker.tsx | 12 ++++- 11 files changed, 110 insertions(+), 39 deletions(-) diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index a99217651a6..0f1de6e0611 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -42,13 +42,13 @@ export const Pages = { }, SubMenu: { submenu: 'Dashboard submenu', - submenuItem: 'Dashboard template variables submenu item', - submenuItemLabels: (item: string) => `Dashboard template variables submenu Label ${item}`, + submenuItem: 'data-testid template variable', + submenuItemLabels: (item: string) => `data-testid Dashboard template variables submenu Label ${item}`, submenuItemValueDropDownValueLinkTexts: (item: string) => - `Dashboard template variables Variable Value DropDown value link text ${item}`, - submenuItemValueDropDownDropDown: 'Dashboard template variables Variable Value DropDown DropDown', + `data-testid Dashboard template variables Variable Value DropDown value link text ${item}`, + submenuItemValueDropDownDropDown: 'Variable options', submenuItemValueDropDownOptionTexts: (item: string) => - `Dashboard template variables Variable Value DropDown option text ${item}`, + `data-testid Dashboard template variables Variable Value DropDown option text ${item}`, }, Settings: { General: { diff --git a/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx b/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx index 96a46604631..196e47f71f7 100644 --- a/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx +++ b/public/app/features/dashboard/components/SubMenu/DashboardLinks.tsx @@ -44,15 +44,15 @@ export const DashboardLinks: FC = ({ dashboard, links }) => { href={sanitizeUrl(linkInfo.href)} target={link.targetBlank ? '_blank' : undefined} rel="noreferrer" - aria-label={selectors.components.DashboardLinks.link} + data-testid={selectors.components.DashboardLinks.link} > - + {linkInfo.title} ); return ( -
+
{link.tooltip ? {linkElement} : linkElement}
); diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx index 2fe5da1470e..01aeca07e61 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -9,6 +9,7 @@ import { Annotations } from './Annotations'; import { SubMenuItems } from './SubMenuItems'; import { DashboardLink } from '../../state/DashboardModel'; import { AnnotationQuery } from '@grafana/data'; +import { css } from '@emotion/css'; interface OwnProps { dashboard: DashboardModel; @@ -47,7 +48,9 @@ class SubMenuUnConnected extends PureComponent { return (
- +
+ + = ( }; }; +const styles = css` + display: flex; + flex-wrap: wrap; + display: contents; +`; + export const SubMenu = connect(mapStateToProps)(SubMenuUnConnected); SubMenu.displayName = 'SubMenu'; diff --git a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx index ce20a183572..72f15ba4db8 100644 --- a/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx +++ b/public/app/features/dashboard/components/SubMenu/SubMenuItems.tsx @@ -9,6 +9,7 @@ interface Props { export const SubMenuItems: FunctionComponent = ({ variables }) => { const [visibleVariables, setVisibleVariables] = useState([]); + useEffect(() => { setVisibleVariables(variables.filter((state) => state.hide !== VariableHide.hideVariable)); }, [variables]); @@ -24,7 +25,7 @@ export const SubMenuItems: FunctionComponent = ({ variables }) => {
diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx index f697ed09932..075908f023d 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx @@ -56,11 +56,11 @@ function setupTestContext({ pickerState = {}, variable = {} }: Args = {}) { } function getSubMenu(text: string) { - return screen.getByLabelText(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(text)); + return screen.getByTestId(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(text)); } function getOption(text: string) { - return screen.getByLabelText(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')); + return screen.getByTestId(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')); } describe('OptionPicker', () => { diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx index edf577eb68a..9c082a44658 100644 --- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx +++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx @@ -75,7 +75,15 @@ export const optionPickerFactory = ; + return ( + + ); } onCancel = () => { @@ -83,12 +91,16 @@ export const optionPickerFactory = ); diff --git a/public/app/features/variables/pickers/PickerRenderer.tsx b/public/app/features/variables/pickers/PickerRenderer.tsx index 2c67d3c4c50..7defe8ceb0b 100644 --- a/public/app/features/variables/pickers/PickerRenderer.tsx +++ b/public/app/features/variables/pickers/PickerRenderer.tsx @@ -37,7 +37,8 @@ function PickerLabel({ variable }: PropsWithChildren): ReactElement | nul @@ -48,7 +49,8 @@ function PickerLabel({ variable }: PropsWithChildren): ReactElement | nul return ( diff --git a/public/app/features/variables/pickers/shared/VariableInput.tsx b/public/app/features/variables/pickers/shared/VariableInput.tsx index 51d1c040957..4ba293aac49 100644 --- a/public/app/features/variables/pickers/shared/VariableInput.tsx +++ b/public/app/features/variables/pickers/shared/VariableInput.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { NavigationKey } from '../types'; -export interface Props { +export interface Props extends Omit, 'onChange' | 'value'> { onChange: (value: string) => void; onNavigate: (key: NavigationKey, clearOthers: boolean) => void; value: string | null; @@ -21,8 +21,10 @@ export class VariableInput extends PureComponent { }; render() { + const { value, id, onNavigate, ...restProps } = this.props; return ( { if (instance) { instance.focus(); @@ -31,9 +33,10 @@ export class VariableInput extends PureComponent { }} type="text" className="gf-form-input" - value={this.props.value ?? ''} + value={value ?? ''} onChange={this.onChange} onKeyDown={this.onKeyDown} + placeholder="Enter variable value" /> ); } diff --git a/public/app/features/variables/pickers/shared/VariableLink.tsx b/public/app/features/variables/pickers/shared/VariableLink.tsx index 70bb9bfb3de..e50fda6d2a5 100644 --- a/public/app/features/variables/pickers/shared/VariableLink.tsx +++ b/public/app/features/variables/pickers/shared/VariableLink.tsx @@ -9,12 +9,16 @@ interface Props { text: string; loading: boolean; onCancel: () => void; + /** + * htmlFor, needed for the label + */ + id: string; } -export const VariableLink: FC = ({ loading, onClick: propsOnClick, text, onCancel }) => { +export const VariableLink: FC = ({ loading, onClick: propsOnClick, text, onCancel, id }) => { const styles = useStyles(getStyles); const onClick = useCallback( - (event: MouseEvent) => { + (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); propsOnClick(); @@ -26,8 +30,9 @@ export const VariableLink: FC = ({ loading, onClick: propsOnClick, text, return (
@@ -36,15 +41,18 @@ export const VariableLink: FC = ({ loading, onClick: propsOnClick, text, } return ( - - - + + ); }; diff --git a/public/app/features/variables/pickers/shared/VariableOptions.tsx b/public/app/features/variables/pickers/shared/VariableOptions.tsx index 4af10003d00..2c6dc2dd19d 100644 --- a/public/app/features/variables/pickers/shared/VariableOptions.tsx +++ b/public/app/features/variables/pickers/shared/VariableOptions.tsx @@ -3,14 +3,19 @@ import { Tooltip } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import { VariableOption } from '../../types'; +import { css, cx } from '@emotion/css'; -export interface Props { +export interface Props extends React.HTMLProps { multi: boolean; values: VariableOption[]; selectedValues: VariableOption[]; highlightIndex: number; onToggle: (option: VariableOption, clearOthers: boolean) => void; onToggleAll: () => void; + /** + * Used for aria-controls + */ + id: string; } export class VariableOptions extends PureComponent { @@ -31,18 +36,20 @@ export class VariableOptions extends PureComponent { } render() { - const { multi, values } = this.props; + // Don't want to pass faulty rest props to the div + const { multi, values, highlightIndex, selectedValues, onToggle, onToggleAll, ...restProps } = this.props; return ( -
+
-
+
    {this.renderMultiToggle()} {values.map((option, index) => this.renderOption(option, index))} -
+
); @@ -54,12 +61,20 @@ export class VariableOptions extends PureComponent { const highlightClass = index === highlightIndex ? `${selectClass} highlighted` : selectClass; return ( - - - - {option.text} - - +
  • + + + + {option.text} + + +
  • ); } @@ -78,7 +93,10 @@ export class VariableOptions extends PureComponent { ? 'variable-options-column-header many-selected' : 'variable-options-column-header' }`} + role="checkbox" + aria-checked={selectedValues.length > 1 ? 'mixed' : 'false'} onClick={this.onToggleAll} + aria-label="Toggle all values" data-placement="top" > @@ -88,3 +106,10 @@ export class VariableOptions extends PureComponent { ); } } + +const listStyles = cx( + 'variable-options-column', + css` + list-style-type: none; + ` +); diff --git a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx index 33fc06f52d4..88ea0c7a501 100644 --- a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx +++ b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx @@ -50,5 +50,15 @@ export function TextBoxVariablePicker({ variable, onVariableChange }: Props): Re } }; - return ; + return ( + + ); }