Themes: Prevent duplicated API call in drawer (#105602)
refactor to separate component, stop propagating radio button event + unit test
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { createTheme, ThemeRegistryItem } from '@grafana/data';
|
||||
|
||||
import { ThemeCard } from './ThemeCard';
|
||||
|
||||
describe('ThemeCard', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
const mockTheme: ThemeRegistryItem = {
|
||||
id: 'dark',
|
||||
name: 'Dark',
|
||||
build: createTheme,
|
||||
};
|
||||
|
||||
it('should only call onSelect once when clicking the radio button dot', async () => {
|
||||
const onSelectMock = jest.fn();
|
||||
|
||||
render(<ThemeCard themeOption={mockTheme} onSelect={onSelectMock} isSelected={false} />);
|
||||
|
||||
// Find the radio button input element
|
||||
const radioButtonInput = screen.getByRole('radio');
|
||||
|
||||
// Click the radio button
|
||||
await user.click(radioButtonInput);
|
||||
|
||||
// Check that onSelect was called only once
|
||||
expect(onSelectMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { FeatureState, GrafanaTheme2, ThemeRegistryItem } from '@grafana/data';
|
||||
import { useTranslate } from '@grafana/i18n';
|
||||
import { TFunction } from '@grafana/i18n/internal';
|
||||
import { FeatureBadge, RadioButtonDot, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { ThemePreview } from '../Theme/ThemePreview';
|
||||
|
||||
interface ThemeCardProps {
|
||||
themeOption: ThemeRegistryItem;
|
||||
isExperimental?: boolean;
|
||||
isSelected?: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export function ThemeCard({ themeOption, isExperimental, isSelected, onSelect }: ThemeCardProps) {
|
||||
const { t } = useTranslate();
|
||||
const theme = themeOption.build();
|
||||
const label = getTranslatedThemeName(themeOption, t);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
// this is a convenience for mouse users. keyboard/screen reader users will use the radio button
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
|
||||
<div className={styles.card} onClick={onSelect}>
|
||||
<div className={styles.header}>
|
||||
<RadioButtonDot
|
||||
id={`theme-${theme.name}`}
|
||||
name={'theme'}
|
||||
label={label}
|
||||
onClick={(event) => {
|
||||
// prevent propagation so that onSelect is only called once when clicking the radio button
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onChange={onSelect}
|
||||
checked={isSelected}
|
||||
/>
|
||||
{isExperimental && <FeatureBadge featureState={FeatureState.experimental} />}
|
||||
</div>
|
||||
<ThemePreview theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
card: css({
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
},
|
||||
}),
|
||||
header: css({
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1),
|
||||
// The RadioButtonDot is not correctly implemented at the moment, missing cursor (And click ability for the label and input)
|
||||
'> label': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
function getTranslatedThemeName(theme: ThemeRegistryItem, t: TFunction) {
|
||||
switch (theme.id) {
|
||||
case 'dark':
|
||||
return t('shared.preferences.theme.dark-label', 'Dark');
|
||||
case 'light':
|
||||
return t('shared.preferences.theme.light-label', 'Light');
|
||||
case 'system':
|
||||
return t('shared.preferences.theme.system-label', 'System preference');
|
||||
default:
|
||||
return theme.name;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { FeatureState, GrafanaTheme2, ThemeRegistryItem } from '@grafana/data';
|
||||
import { GrafanaTheme2, ThemeRegistryItem } from '@grafana/data';
|
||||
import { Trans, useTranslate } from '@grafana/i18n';
|
||||
import { TFunction } from '@grafana/i18n/internal';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Drawer, FeatureBadge, RadioButtonDot, TextLink, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { Drawer, TextLink, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { changeTheme } from 'app/core/services/theme';
|
||||
|
||||
import { ThemePreview } from '../Theme/ThemePreview';
|
||||
|
||||
import { ThemeCard } from './ThemeCard';
|
||||
import { getSelectableThemes } from './getSelectableThemes';
|
||||
|
||||
interface Props {
|
||||
@@ -63,38 +61,6 @@ export function ThemeSelectorDrawer({ onClose }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
interface ThemeCardProps {
|
||||
themeOption: ThemeRegistryItem;
|
||||
isExperimental?: boolean;
|
||||
isSelected?: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
function ThemeCard({ themeOption, isExperimental, isSelected, onSelect }: ThemeCardProps) {
|
||||
const { t } = useTranslate();
|
||||
const theme = themeOption.build();
|
||||
const label = getTranslatedThemeName(themeOption, t);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
// this is a convenience for mouse users. keyboard/screen reader users will use the radio button
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
|
||||
<div className={styles.card} onClick={onSelect}>
|
||||
<div className={styles.header}>
|
||||
<RadioButtonDot
|
||||
id={`theme-${theme.name}`}
|
||||
name={'theme'}
|
||||
label={label}
|
||||
onChange={onSelect}
|
||||
checked={isSelected}
|
||||
/>
|
||||
{isExperimental && <FeatureBadge featureState={FeatureState.experimental} />}
|
||||
</div>
|
||||
<ThemePreview theme={theme} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
grid: css({
|
||||
@@ -103,40 +69,5 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
gridAutoRows: `250px`,
|
||||
gap: theme.spacing(2),
|
||||
}),
|
||||
card: css({
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
border: `1px solid ${theme.colors.border.medium}`,
|
||||
},
|
||||
}),
|
||||
header: css({
|
||||
alignItems: 'center',
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(1),
|
||||
// The RadioButtonDot is not correctly implemented at the moment, missing cursor (And click ability for the label and input)
|
||||
'> label': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
function getTranslatedThemeName(theme: ThemeRegistryItem, t: TFunction) {
|
||||
switch (theme.id) {
|
||||
case 'dark':
|
||||
return t('shared.preferences.theme.dark-label', 'Dark');
|
||||
case 'light':
|
||||
return t('shared.preferences.theme.light-label', 'Light');
|
||||
case 'system':
|
||||
return t('shared.preferences.theme.system-label', 'System preference');
|
||||
default:
|
||||
return theme.name;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user