mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana-UI: Refactor Checkbox to fix alignment issues (#36164)
* Grafana-UI: Fix Checkbox vertical layout issues * reorganise css * WIP for fixing checkboxes in manage dashboards * Cleanup checkbox and fix issue in Dashboard manage page * update tests
This commit is contained in:
parent
f45d0a0b7a
commit
4c0acf335b
@ -18,54 +18,62 @@ export const Controlled = () => {
|
|||||||
const [checked, setChecked] = useState(false);
|
const [checked, setChecked] = useState(false);
|
||||||
const onChange = useCallback((e) => setChecked(e.currentTarget.checked), [setChecked]);
|
const onChange = useCallback((e) => setChecked(e.currentTarget.checked), [setChecked]);
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<div>
|
||||||
value={checked}
|
<Checkbox
|
||||||
onChange={onChange}
|
value={checked}
|
||||||
label="Skip TLS cert validation"
|
onChange={onChange}
|
||||||
description="Set to true if you want to skip TLS cert validation"
|
label="Skip TLS cert validation"
|
||||||
/>
|
description="Set to true if you want to skip TLS cert validation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uncontrolled = () => {
|
export const uncontrolled = () => {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<div>
|
||||||
defaultChecked={true}
|
|
||||||
label="Skip TLS cert validation"
|
|
||||||
description="Set to true if you want to skip TLS cert validation"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StackedList = () => {
|
|
||||||
return (
|
|
||||||
<VerticalGroup>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
label="Skip TLS cert validation"
|
label="Skip TLS cert validation"
|
||||||
description="Set to true if you want to skip TLS cert validation"
|
description="Set to true if you want to skip TLS cert validation"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
</div>
|
||||||
defaultChecked={true}
|
);
|
||||||
label="Another checkbox"
|
};
|
||||||
description="Another long description that does not make any sense"
|
|
||||||
/>
|
export const StackedList = () => {
|
||||||
<Checkbox
|
return (
|
||||||
defaultChecked={true}
|
<div>
|
||||||
label="Another checkbox times 2"
|
<VerticalGroup>
|
||||||
description="Another long description that does not make any sense or does it?"
|
<Checkbox
|
||||||
/>
|
defaultChecked={true}
|
||||||
</VerticalGroup>
|
label="Skip TLS cert validation"
|
||||||
|
description="Set to true if you want to skip TLS cert validation"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={true}
|
||||||
|
label="Another checkbox"
|
||||||
|
description="Another long description that does not make any sense"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked={true}
|
||||||
|
label="Another checkbox times 2"
|
||||||
|
description="Another long description that does not make any sense or does it?"
|
||||||
|
/>
|
||||||
|
</VerticalGroup>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InAField = () => {
|
export const InAField = () => {
|
||||||
return (
|
return (
|
||||||
<Field
|
<div>
|
||||||
label="Hidden"
|
<Field
|
||||||
description="Annotation queries can be toggled on or of at the top of the dashboard. With this option checked this toggle will be hidden."
|
label="Hidden"
|
||||||
>
|
description="Annotation queries can be toggled on or of at the top of the dashboard. With this option checked this toggle will be hidden."
|
||||||
<Checkbox name="hide" id="hide" defaultChecked={true} />
|
>
|
||||||
</Field>
|
<Checkbox name="hide" id="hide" defaultChecked={true} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -44,34 +44,21 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
|
|
||||||
export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||||
const labelStyles = getLabelStyles(theme);
|
const labelStyles = getLabelStyles(theme);
|
||||||
const checkboxSize = '16px';
|
const checkboxSize = 2;
|
||||||
|
const labelPadding = 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: cx(
|
|
||||||
labelStyles.label,
|
|
||||||
css`
|
|
||||||
padding-left: ${theme.spacing(1)};
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
`
|
|
||||||
),
|
|
||||||
description: cx(
|
|
||||||
labelStyles.description,
|
|
||||||
css`
|
|
||||||
line-height: ${theme.typography.bodySmall.lineHeight};
|
|
||||||
padding-left: ${theme.spacing(1)};
|
|
||||||
`
|
|
||||||
),
|
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: ${checkboxSize};
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
min-height: ${theme.spacing(3)};
|
font-size: 0;
|
||||||
`,
|
`,
|
||||||
input: css`
|
input: css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100% !important; // global styles unset this
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
||||||
@ -101,6 +88,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@ -114,6 +102,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
&:disabled + span {
|
&:disabled + span {
|
||||||
background-color: ${theme.colors.action.disabledBackground};
|
background-color: ${theme.colors.action.disabledBackground};
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${theme.colors.action.disabledBackground};
|
background-color: ${theme.colors.action.disabledBackground};
|
||||||
}
|
}
|
||||||
@ -124,22 +113,40 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
checkmark: css`
|
checkmark: css`
|
||||||
|
position: relative; /* Checkbox should be layered on top of the invisible input so it recieves :hover */
|
||||||
|
z-index: 2;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: ${checkboxSize};
|
width: ${theme.spacing(checkboxSize)};
|
||||||
height: ${checkboxSize};
|
height: ${theme.spacing(checkboxSize)};
|
||||||
border-radius: ${theme.shape.borderRadius()};
|
border-radius: ${theme.shape.borderRadius()};
|
||||||
margin-right: ${theme.spacing(1)};
|
|
||||||
background: ${theme.components.input.background};
|
background: ${theme.components.input.background};
|
||||||
border: 1px solid ${theme.components.input.borderColor};
|
border: 1px solid ${theme.components.input.borderColor};
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-color: ${theme.components.input.borderHover};
|
border-color: ${theme.components.input.borderHover};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
label: cx(
|
||||||
|
labelStyles.label,
|
||||||
|
css`
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding-left: ${theme.spacing(labelPadding)};
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
`
|
||||||
|
),
|
||||||
|
description: cx(
|
||||||
|
labelStyles.description,
|
||||||
|
css`
|
||||||
|
line-height: ${theme.typography.bodySmall.lineHeight};
|
||||||
|
padding-left: ${theme.spacing(checkboxSize + labelPadding)};
|
||||||
|
margin-top: 0; /* The margin effectively comes from the top: -2px on the label above it */
|
||||||
|
`
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,34 +1,19 @@
|
|||||||
import React, { FC, memo } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { Checkbox } from '@grafana/ui';
|
||||||
import { Checkbox, stylesFactory } from '@grafana/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onClick: any;
|
onClick?: React.MouseEventHandler<HTMLInputElement>;
|
||||||
|
className?: string;
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchCheckbox: FC<Props> = memo(({ onClick, checked = false, editable = false }) => {
|
export const SearchCheckbox: FC<Props> = memo(({ onClick, className, checked = false, editable = false }) => {
|
||||||
const styles = getStyles();
|
|
||||||
|
|
||||||
return editable ? (
|
return editable ? (
|
||||||
<div onClick={onClick} className={styles.wrapper}>
|
<div onClick={onClick} className={className}>
|
||||||
<Checkbox value={checked} />
|
<Checkbox value={checked} />
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => ({
|
|
||||||
wrapper: css`
|
|
||||||
height: 21px;
|
|
||||||
& > label {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
& > input {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
SearchCheckbox.displayName = 'SearchCheckbox';
|
SearchCheckbox.displayName = 'SearchCheckbox';
|
||||||
|
@ -39,7 +39,7 @@ describe('SearchItem', () => {
|
|||||||
expect(screen.getAllByText('Test 1')).toHaveLength(1);
|
expect(screen.getAllByText('Test 1')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should mark item as checked', () => {
|
it('should toggle items when checked', () => {
|
||||||
const mockedOnToggleChecked = jest.fn();
|
const mockedOnToggleChecked = jest.fn();
|
||||||
setup({ editable: true, onToggleChecked: mockedOnToggleChecked });
|
setup({ editable: true, onToggleChecked: mockedOnToggleChecked });
|
||||||
const checkbox = screen.getByRole('checkbox');
|
const checkbox = screen.getByRole('checkbox');
|
||||||
@ -47,6 +47,10 @@ describe('SearchItem', () => {
|
|||||||
fireEvent.click(checkbox);
|
fireEvent.click(checkbox);
|
||||||
expect(mockedOnToggleChecked).toHaveBeenCalledTimes(1);
|
expect(mockedOnToggleChecked).toHaveBeenCalledTimes(1);
|
||||||
expect(mockedOnToggleChecked).toHaveBeenCalledWith(data);
|
expect(mockedOnToggleChecked).toHaveBeenCalledWith(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark items as checked', () => {
|
||||||
|
setup({ editable: true, item: { ...data, checked: true } });
|
||||||
expect(screen.getByRole('checkbox')).toBeChecked();
|
expect(screen.getByRole('checkbox')).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,9 +34,11 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
|||||||
[onTagSelected]
|
[onTagSelected]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleItem = useCallback(
|
const handleCheckboxClick = useCallback(
|
||||||
(event: React.MouseEvent) => {
|
(ev: React.MouseEvent) => {
|
||||||
event.preventDefault();
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
if (onToggleChecked) {
|
if (onToggleChecked) {
|
||||||
onToggleChecked(item);
|
onToggleChecked(item);
|
||||||
}
|
}
|
||||||
@ -54,7 +56,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
|
|||||||
className={styles.container}
|
className={styles.container}
|
||||||
>
|
>
|
||||||
<Card.Figure align={'center'} className={styles.checkbox}>
|
<Card.Figure align={'center'} className={styles.checkbox}>
|
||||||
<SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} />
|
<SearchCheckbox editable={editable} checked={item.checked} onClick={handleCheckboxClick} />
|
||||||
</Card.Figure>
|
</Card.Figure>
|
||||||
<Card.Meta separator={''}>
|
<Card.Meta separator={''}>
|
||||||
<span className={styles.metaContainer}>
|
<span className={styles.metaContainer}>
|
||||||
|
@ -29,10 +29,12 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
|
|||||||
onSectionClick(section);
|
onSectionClick(section);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSectionChecked = useCallback(
|
const handleCheckboxClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(ev: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
console.log('section header handleCheckboxClick');
|
||||||
e.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
if (onToggleChecked) {
|
if (onToggleChecked) {
|
||||||
onToggleChecked(section);
|
onToggleChecked(section);
|
||||||
}
|
}
|
||||||
@ -46,7 +48,12 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
|
|||||||
onClick={onSectionExpand}
|
onClick={onSectionExpand}
|
||||||
aria-label={section.expanded ? `Collapse folder ${section.id}` : `Expand folder ${section.id}`}
|
aria-label={section.expanded ? `Collapse folder ${section.id}` : `Expand folder ${section.id}`}
|
||||||
>
|
>
|
||||||
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
|
<SearchCheckbox
|
||||||
|
className={styles.checkbox}
|
||||||
|
editable={editable}
|
||||||
|
checked={section.checked}
|
||||||
|
onClick={handleCheckboxClick}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.icon}>
|
<div className={styles.icon}>
|
||||||
<Icon name={getSectionIcon(section)} />
|
<Icon name={getSectionIcon(section)} />
|
||||||
@ -90,6 +97,9 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
|
|||||||
'pointer',
|
'pointer',
|
||||||
{ selected }
|
{ selected }
|
||||||
),
|
),
|
||||||
|
checkbox: css`
|
||||||
|
padding: 0 ${sm} 0 0;
|
||||||
|
`,
|
||||||
icon: css`
|
icon: css`
|
||||||
padding: 0 ${sm} 0 ${editable ? 0 : sm};
|
padding: 0 ${sm} 0 ${editable ? 0 : sm};
|
||||||
`,
|
`,
|
||||||
|
Loading…
Reference in New Issue
Block a user