GrafanaUI: Add external border radius mixin (#100266)

* Add external border radius mixin

* Add getInternalRadius mixin for inverse use case

* Update border radius fns with clearler types and JSDoc annotations

* tidy up interface, apply correctly in radio buttons, add internal story

* connect padding/offset in radiobuttongroup/radiobutton

* split out offset into its own param

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Ed Poole
2025-03-10 12:09:48 +00:00
committed by GitHub
co-authored by Ashley Harrison
parent 852243c8f0
commit 98e4d57bcd
5 changed files with 190 additions and 5 deletions
@@ -5,10 +5,11 @@ import { GrafanaTheme2 } from '@grafana/data';
import { StringSelector, selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins';
import { getFocusStyles, getInternalRadius, getMouseFocusStyles } from '../../../themes/mixins';
import { Tooltip } from '../../Tooltip/Tooltip';
import { getPropertiesForButtonSize } from '../commonStyles';
export const RADIO_GROUP_PADDING = 2;
export type RadioButtonSize = 'sm' | 'md';
export interface RadioButtonProps {
@@ -130,7 +131,7 @@ const getRadioButtonStyles = (theme: GrafanaTheme2, size: RadioButtonSize, fullW
lineHeight: `${labelHeight}px`,
color: textColor,
padding: theme.spacing(0, padding),
borderRadius: theme.shape.radius.default,
borderRadius: getInternalRadius(theme, RADIO_GROUP_PADDING),
background: theme.colors.background.primary,
cursor: 'pointer',
userSelect: 'none',
@@ -7,8 +7,7 @@ import { GrafanaTheme2, SelectableValue, toIconName } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { Icon } from '../../Icon/Icon';
import { RadioButtonSize, RadioButton } from './RadioButton';
import { RadioButtonSize, RadioButton, RADIO_GROUP_PADDING } from './RadioButton';
export interface RadioButtonGroupProps<T> {
value?: T;
id?: string;
@@ -119,7 +118,7 @@ const getStyles = (theme: GrafanaTheme2) => {
flexWrap: 'nowrap',
border: `1px solid ${theme.components.input.borderColor}`,
borderRadius: theme.shape.radius.default,
padding: '2px',
padding: RADIO_GROUP_PADDING,
'&:hover': {
borderColor: theme.components.input.borderHover,
},
@@ -0,0 +1,46 @@
import { Meta, StoryFn } from '@storybook/react';
import { BorderRadiusContainer } from './BorderRadius';
const meta: Meta = {
title: 'Docs Overview/Theme',
component: BorderRadiusContainer,
decorators: [],
parameters: {
layout: 'centered',
},
args: {
referenceBorderRadius: 20,
referenceBorderWidth: 10,
offset: 0,
borderWidth: 2,
},
argTypes: {
offset: {
control: {
min: 0,
},
},
referenceBorderRadius: {
control: {
min: 0,
},
},
referenceBorderWidth: {
control: {
min: 0,
},
},
borderWidth: {
control: {
min: 0,
},
},
},
};
export const OffsetBorderRadius: StoryFn<typeof BorderRadiusContainer> = (args) => {
return <BorderRadiusContainer {...args} />;
};
export default meta;
@@ -0,0 +1,87 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { getInternalRadius, getExternalRadius } from '../../themes/mixins';
import { Stack } from '../Layout/Stack/Stack';
import { Text } from '../Text/Text';
interface DemoBoxProps {
referenceBorderRadius: number;
referenceBorderWidth: number;
offset: number;
borderWidth: number;
}
export const BorderRadiusContainer = ({
referenceBorderRadius,
referenceBorderWidth,
offset,
borderWidth,
}: DemoBoxProps) => {
const styles = useStyles2(getStyles, referenceBorderRadius, referenceBorderWidth, offset, borderWidth);
return (
<Stack direction="column" alignItems="center" gap={4}>
<Stack alignItems="center">
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<Text variant="code">getInternalRadius</Text>
<div className={styles.baseForInternal}>
<div className={styles.internalContainer} />
</div>
</Stack>
<Stack alignItems="center">
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<Text variant="code">getExternalRadius</Text>
<div className={styles.externalContainer}>
<div className={styles.baseForExternal} />
</div>
</Stack>
</Stack>
);
};
const getStyles = (
theme: GrafanaTheme2,
referenceBorderRadius: number,
referenceBorderWidth: number,
offset: number,
borderWidth: number
) => ({
baseForInternal: css({
backgroundColor: theme.colors.action.disabledBackground,
border: `${referenceBorderWidth}px dashed ${theme.colors.action.disabledText}`,
borderRadius: referenceBorderRadius,
display: 'flex',
height: '80px',
padding: offset,
width: '300px',
}),
baseForExternal: css({
backgroundColor: theme.colors.action.disabledBackground,
border: `${referenceBorderWidth}px dashed ${theme.colors.action.disabledText}`,
borderRadius: referenceBorderRadius,
height: '80px',
flex: 1,
width: '300px',
}),
internalContainer: css({
backgroundColor: theme.colors.background.primary,
border: `${borderWidth}px solid ${theme.colors.primary.main}`,
borderRadius: getInternalRadius(theme, offset, {
parentBorderRadius: referenceBorderRadius,
parentBorderWidth: referenceBorderWidth,
}),
flex: 1,
}),
externalContainer: css({
border: `${borderWidth}px solid ${theme.colors.primary.main}`,
borderRadius: getExternalRadius(theme, offset, {
childBorderRadius: referenceBorderRadius,
selfBorderWidth: borderWidth,
}),
display: 'flex',
flex: 1,
padding: offset,
}),
});
+52
View File
@@ -82,3 +82,55 @@ export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({
borderRadius: theme.shape.radius.default,
zIndex: theme.zIndex.tooltip,
});
interface ExternalRadiusAdditionalOptions {
selfBorderWidth?: number;
childBorderRadius?: number;
}
/**
* Calculates a border radius for an element, based on border radius of its child.
*
* @param theme
* @param offset - The distance to offset from the child element, should be >= 0.
* @param additionalOptions
* @param additionalOptions.selfBorderWidth - The border width of the element itself (default: 1)
* @param additionalOptions.childBorderRadius - The border radius of the child element (default: theme default radius)
* @returns A CSS calc() expression that returns the relative external radius value
*/
export const getExternalRadius = (
theme: GrafanaTheme2,
offset: number,
additionalOptions: ExternalRadiusAdditionalOptions = {}
) => {
const { selfBorderWidth = 1, childBorderRadius } = additionalOptions;
const childBorderRadiusPx = childBorderRadius !== undefined ? `${childBorderRadius}px` : theme.shape.radius.default;
return `calc(max(0px, ${childBorderRadiusPx} + ${offset}px + ${selfBorderWidth}px))`;
};
interface InternalRadiusAdditionalOptions {
parentBorderWidth?: number;
parentBorderRadius?: number;
}
/**
* Calculates a border radius for an element, based on border radius of its parent.
*
* @param theme
* @param offset - The distance to offset from the parent element, should be >= 0.
* @param additionalOptions
* @param additionalOptions.parentBorderWidth - The border width of the parent element (default: 1)
* @param additionalOptions.parentBorderRadius - The border radius of the parent element (default: theme default radius)
* @returns A CSS calc() expression that returns the relative internal radius value
*/
export const getInternalRadius = (
theme: GrafanaTheme2,
offset: number,
additionalOptions: InternalRadiusAdditionalOptions = {}
) => {
const { parentBorderWidth = 1, parentBorderRadius } = additionalOptions;
const parentBorderRadiusPx =
parentBorderRadius !== undefined ? `${parentBorderRadius}px` : theme.shape.radius.default;
return `calc(max(0px, ${parentBorderRadiusPx} - ${offset}px - ${parentBorderWidth}px))`;
};