From 98e4d57bcd1344b9392a6d9e68653c0a1177c95b Mon Sep 17 00:00:00 2001 From: Ed Poole Date: Mon, 10 Mar 2025 12:09:48 +0000 Subject: [PATCH] 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 --- .../Forms/RadioButtonGroup/RadioButton.tsx | 5 +- .../RadioButtonGroup/RadioButtonGroup.tsx | 5 +- .../BorderRadius.internal.story.tsx | 46 ++++++++++ .../components/ThemeDemos/BorderRadius.tsx | 87 +++++++++++++++++++ packages/grafana-ui/src/themes/mixins.ts | 52 +++++++++++ 5 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx create mode 100644 packages/grafana-ui/src/components/ThemeDemos/BorderRadius.tsx diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx index 898247dff7d..92f1c0d494e 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -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', diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx index 2cd47626db9..d615ca4d696 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx @@ -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 { 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, }, diff --git a/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx new file mode 100644 index 00000000000..c938199176c --- /dev/null +++ b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx @@ -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 = (args) => { + return ; +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.tsx b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.tsx new file mode 100644 index 00000000000..7419ae28fe6 --- /dev/null +++ b/packages/grafana-ui/src/components/ThemeDemos/BorderRadius.tsx @@ -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 ( + + + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} + getInternalRadius +
+
+
+ + + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} + getExternalRadius +
+
+
+ + + ); +}; + +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, + }), +}); diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index 7e9e42179b0..5127047ada0 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -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))`; +};