diff --git a/src-docs/src/views/badge/notification_badge.tsx b/src-docs/src/views/badge/notification_badge.tsx index 99c7f9e46cd..009accce63b 100644 --- a/src-docs/src/views/badge/notification_badge.tsx +++ b/src-docs/src/views/badge/notification_badge.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { EuiNotificationBadge } from '../../../../src/components/badge/notification_badge'; +import { EuiFlexGroup, EuiNotificationBadge } from '../../../../src/components'; -export default () => 3; +export default () => ( + + 1 + 2 + 3 + +); diff --git a/src-docs/src/views/filter_group/filter_group_multi.js b/src-docs/src/views/filter_group/filter_group_multi.js index 5ea1b757c4b..bf9be4d07c2 100644 --- a/src-docs/src/views/filter_group/filter_group_multi.js +++ b/src-docs/src/views/filter_group/filter_group_multi.js @@ -63,6 +63,7 @@ export default () => { const button = ( item.checked !== 'off').length} diff --git a/src/components/badge/__snapshots__/badge.test.tsx.snap b/src/components/badge/__snapshots__/badge.test.tsx.snap index 25dccfe71b9..80cce86b192 100644 --- a/src/components/badge/__snapshots__/badge.test.tsx.snap +++ b/src/components/badge/__snapshots__/badge.test.tsx.snap @@ -3,9 +3,8 @@ exports[`EuiBadge is disabled 1`] = ` { - const { euiTheme, colorMode } = euiThemeContext; + const { euiTheme } = euiThemeContext; + const badgeColors = euiBadgeColors(euiThemeContext); return { euiBadge: css` @@ -72,24 +73,28 @@ export const euiBadgeStyles = (euiThemeContext: UseEuiTheme) => { cursor: not-allowed; } `, + + // Colors + default: css(badgeColors.default), + hollow: css` + color: ${badgeColors.hollow.color}; + background-color: ${badgeColors.hollow.backgroundColor}; + border-color: ${badgeColors.hollow.borderColor}; + `, + primary: css(badgeColors.primary), + accent: css(badgeColors.accent), + warning: css(badgeColors.warning), + danger: css(badgeColors.danger), + success: css(badgeColors.success), disabled: css` /* stylelint-disable declaration-no-important */ /* Using !important to override inline styles */ - color: ${euiButtonColor(euiThemeContext, 'disabled').color} !important; - background-color: ${euiButtonColor(euiThemeContext, 'disabled') - .backgroundColor} !important; + color: ${badgeColors.disabled.color} !important; + background-color: ${badgeColors.disabled.backgroundColor} !important; /* stylelint-enable declaration-no-important */ `, - // Hollow has a border and is mostly used for autocompleters. - hollow: css` - background-color: ${euiTheme.colors.emptyShade}; - border-color: ${colorMode === 'DARK' - ? tint(euiTheme.border.color, 0.15) - : euiTheme.border.color}; - color: ${euiTheme.colors.text}; - `, // Content wrapper euiBadge__content: css` diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx index 1b9fd2afda4..e9bb385aff0 100644 --- a/src/components/badge/badge.tsx +++ b/src/components/badge/badge.tsx @@ -16,22 +16,18 @@ import React, { useMemo, } from 'react'; import classNames from 'classnames'; -import chroma from 'chroma-js'; import { CommonProps, ExclusiveUnion, PropsOf } from '../common'; import { useEuiTheme, - UseEuiTheme, getSecureRelForTarget, - isColorDark, wcagContrastMin, } from '../../services'; import { EuiInnerText } from '../inner_text'; import { EuiIcon, IconType } from '../icon'; -import { chromaValid, parseColor } from '../color_picker/utils'; import { validateHref } from '../../services/security/href_validator'; +import { getTextColor, getColorContrast, getIsValidColor } from './color_utils'; import { euiBadgeStyles } from './badge.styles'; -import { euiButtonFillColor } from '../../themes/amsterdam/global_styling/mixins'; export const ICON_SIDES = ['left', 'right'] as const; type IconSide = (typeof ICON_SIDES)[number]; @@ -132,83 +128,49 @@ export const EuiBadge: FunctionComponent = ({ const isHrefValid = !href || validateHref(href); const isDisabled = _isDisabled || !isHrefValid; + const isNamedColor = COLORS.includes(color as BadgeColor); - const optionalCustomStyles = useMemo(() => { - let textColor = null; - let contrastRatio = null; - let colorHex = null; + const customColorStyles = useMemo(() => { + // Named colors set their styles via Emotion CSS and not inline styles + if (isNamedColor) return style; + // Do our best to ensure custom colors provide sufficient contrast try { - // Check if a valid color name was provided - if (COLORS.includes(color as BadgeColor)) { - // Get the hex equivalent for the provided color name - switch (color) { - case 'hollow': - return style; // hollow uses its own Emotion class - case 'default': - colorHex = euiTheme.euiTheme.colors.lightShade; - break; - default: - type RemainingColors = - | 'primary' - | 'success' - | 'accent' - | 'warning' - | 'danger'; - colorHex = euiButtonFillColor( - euiTheme, - color as RemainingColors - ).backgroundColor; - break; - } - - // Set dark or light text color based upon best contrast - textColor = setTextColor(euiTheme, colorHex); - - return { - backgroundColor: colorHex, - color: textColor, - ...style, - }; - } else { - // This is a custom color- let's do our best to ensure that it provides sufficient contrast - - // Set dark or light text color based upon best contrast - textColor = setTextColor(euiTheme, color); - - // Check the contrast - contrastRatio = getColorContrast(textColor, color); - - if (contrastRatio < wcagContrastMin) { - // It's low contrast, so lets show a warning in the console - console.warn( - 'Warning: ', - color, - ' badge has low contrast of ', - contrastRatio.toFixed(2), - '. Should be above ', - wcagContrastMin, - '.' - ); - } + // Set dark or light text color based upon best contrast + const textColor = getTextColor(euiTheme, color); - return { - backgroundColor: color, - color: textColor, - ...style, - }; + // Check the contrast ratio. If it's low contrast, emit a console awrning + const contrastRatio = getColorContrast(textColor, color); + if (contrastRatio < wcagContrastMin) { + console.warn( + `Warning: ${color} badge has a low contrast of ${contrastRatio.toFixed( + 2 + )}. Should be above ${wcagContrastMin}.` + ); } + + return { + backgroundColor: color, + color: textColor, + ...style, + }; } catch (err) { - handleInvalidColor(color); + if (!getIsValidColor(color)) { + console.warn( + 'EuiBadge expects a valid color. This can either be a three or six ' + + `character hex value, rgb(a) value, hsv value, hollow, or one of the following: ${COLORS}. ` + + `Instead got ${color}.` + ); + } } - }, [color, style, euiTheme]); + }, [color, isNamedColor, style, euiTheme]); const styles = euiBadgeStyles(euiTheme); const cssStyles = [ styles.euiBadge, + isNamedColor && styles[color as BadgeColor], (onClick || href) && !iconOnClick && styles.clickable, isDisabled && styles.disabled, - color === 'hollow' && styles.hollow, ]; const textCssStyles = [ styles.text.euiBadge__text, @@ -307,7 +269,7 @@ export const EuiBadge: FunctionComponent = ({ if (iconOnClick) { return onClick || href ? ( - + {iconSide === 'left' && optionalIcon} @@ -335,7 +297,7 @@ export const EuiBadge: FunctionComponent = ({ = ({ aria-label={onClickAriaLabel} className={classes} css={cssStyles} - style={optionalCustomStyles} + style={customColorStyles} ref={ref as Ref} title={innerText} {...(relObj as HTMLAttributes)} @@ -372,7 +334,7 @@ export const EuiBadge: FunctionComponent = ({ = ({ ); } }; - -function getColorContrast(textColor: string, color: string) { - const contrastValue = chroma.contrast(textColor, color); - return contrastValue; -} - -function setTextColor({ euiTheme }: UseEuiTheme, bgColor: string) { - const textColor = isColorDark(...chroma(bgColor).rgb()) - ? euiTheme.colors.ghost - : euiTheme.colors.ink; - - return textColor; -} - -function handleInvalidColor(color: null | BadgeColor | string) { - const isNamedColor = COLORS.includes(color as BadgeColor); - const isValidColorString = color && chromaValid(parseColor(color) || ''); - if (!isNamedColor && !isValidColorString) { - console.warn( - 'EuiBadge expects a valid color. This can either be a three or six ' + - `character hex value, rgb(a) value, hsv value, hollow, or one of the following: ${COLORS}. ` + - `Instead got ${color}.` - ); - } -} diff --git a/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap index e77c89dd12f..f5c142d69ef 100644 --- a/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap +++ b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap @@ -25,8 +25,7 @@ exports[`EuiBadgeGroup is rendered 1`] = ` data-test-subj="test subject string" > { const { euiTheme, colorMode } = euiThemeContext; + const badgeColors = euiBadgeColors(euiThemeContext); return { euiBetaBadge: css` @@ -39,16 +41,13 @@ export const euiBetaBadgeStyles = (euiThemeContext: UseEuiTheme) => { } `, // Colors - accent: css` - ${getBadgeColors(euiTheme.colors.accentText, euiThemeContext)} - `, - subdued: css` - ${getBadgeColors(tint(euiTheme.colors.lightShade, 0.3), euiThemeContext)} - `, + accent: css(badgeColors.accentText), + subdued: css(badgeColors.subdued), hollow: css` - ${getBadgeColors(euiTheme.colors.emptyShade, euiThemeContext)} + color: ${badgeColors.hollow.color}; + background-color: ${badgeColors.hollow.backgroundColor}; box-shadow: inset 0 0 0 ${euiTheme.border.width.thin} - ${euiTheme.border.color}; + ${badgeColors.hollow.borderColor}; `, // Font sizes m: css` @@ -93,18 +92,3 @@ export const euiBetaBadgeStyles = (euiThemeContext: UseEuiTheme) => { `, }; }; - -// Util for detecting text color based on badge bg color -export const getBadgeColors = ( - backgroundColor: string, - { euiTheme }: UseEuiTheme -) => { - const textColor = isColorDark(...hexToRgb(backgroundColor)) - ? euiTheme.colors.ghost - : euiTheme.colors.ink; - - return ` - background-color: ${backgroundColor}; - color: ${textColor}; - `; -}; diff --git a/src/components/badge/color_utils.ts b/src/components/badge/color_utils.ts new file mode 100644 index 00000000000..351404bce19 --- /dev/null +++ b/src/components/badge/color_utils.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import chroma from 'chroma-js'; + +import { UseEuiTheme, isColorDark, tint } from '../../services'; +import { + euiButtonColor, + euiButtonFillColor, +} from '../../themes/amsterdam/global_styling/mixins'; +import { chromaValid, parseColor } from '../color_picker/utils'; + +export const euiBadgeColors = (euiThemeContext: UseEuiTheme) => { + const { euiTheme, colorMode } = euiThemeContext; + + return { + // Colors shared between buttons and badges + primary: euiButtonFillColor(euiThemeContext, 'primary'), + success: euiButtonFillColor(euiThemeContext, 'success'), + warning: euiButtonFillColor(euiThemeContext, 'warning'), + danger: euiButtonFillColor(euiThemeContext, 'danger'), + accent: euiButtonFillColor(euiThemeContext, 'accent'), + disabled: euiButtonColor(euiThemeContext, 'disabled'), + // Colors unique to badges + default: getBadgeColors(euiThemeContext, euiTheme.colors.lightShade), + // Hollow has a border and is used for autocompleters and beta badges + hollow: { + ...getBadgeColors(euiThemeContext, euiTheme.colors.emptyShade), + borderColor: + colorMode === 'DARK' + ? tint(euiTheme.border.color, 0.15) + : euiTheme.border.color, + }, + // Colors used by beta and notification badges + subdued: getBadgeColors( + euiThemeContext, + tint(euiTheme.colors.lightShade, 0.3) + ), + accentText: getBadgeColors(euiThemeContext, euiTheme.colors.accentText), + }; +}; + +export const getBadgeColors = ( + euiThemeContext: UseEuiTheme, + backgroundColor: string +) => { + const color = getTextColor(euiThemeContext, backgroundColor); + + return { + backgroundColor, + color, + }; +}; + +export const getTextColor = ({ euiTheme }: UseEuiTheme, bgColor: string) => { + const textColor = isColorDark(...chroma(bgColor).rgb()) + ? euiTheme.colors.ghost + : euiTheme.colors.ink; + + return textColor; +}; + +export const getColorContrast = (textColor: string, color: string) => { + return chroma.contrast(textColor, color); +}; + +export const getIsValidColor = (color?: string) => { + return chromaValid(parseColor(color || '') || ''); +}; diff --git a/src/components/badge/notification_badge/__snapshots__/badge_notification.test.tsx.snap b/src/components/badge/notification_badge/__snapshots__/badge_notification.test.tsx.snap index 53953cf5180..532aebb1b50 100644 --- a/src/components/badge/notification_badge/__snapshots__/badge_notification.test.tsx.snap +++ b/src/components/badge/notification_badge/__snapshots__/badge_notification.test.tsx.snap @@ -26,6 +26,14 @@ exports[`EuiNotificationBadge props color subdued is rendered 1`] = ` `; +exports[`EuiNotificationBadge props color success is rendered 1`] = ` + + 5 + +`; + exports[`EuiNotificationBadge props size m is rendered 1`] = ` { const { euiTheme } = euiThemeContext; + const badgeColors = euiBadgeColors(euiThemeContext); return { euiNotificationBadge: css` @@ -54,11 +54,8 @@ export const euiNotificationBadgeStyles = (euiThemeContext: UseEuiTheme) => { ${logicalCSS('min-width', euiTheme.size.l)} `, // Colors - accent: css` - ${getBadgeColors(euiTheme.colors.accentText, euiThemeContext)} - `, - subdued: css` - ${getBadgeColors(tint(euiTheme.colors.lightShade, 0.3), euiThemeContext)} - `, + accent: css(badgeColors.accentText), + success: css(badgeColors.success), + subdued: css(badgeColors.subdued), }; }; diff --git a/src/components/badge/notification_badge/badge_notification.tsx b/src/components/badge/notification_badge/badge_notification.tsx index a4e9ad8d746..7a1ea13b684 100644 --- a/src/components/badge/notification_badge/badge_notification.tsx +++ b/src/components/badge/notification_badge/badge_notification.tsx @@ -13,7 +13,7 @@ import { useEuiTheme } from '../../../services'; import { euiNotificationBadgeStyles } from './badge_notification.styles'; -export const COLORS = ['accent', 'subdued'] as const; +export const COLORS = ['accent', 'subdued', 'success'] as const; export type BadgeNotificationColor = (typeof COLORS)[number]; export const SIZES = ['s', 'm'] as const; diff --git a/src/components/filter_group/__snapshots__/filter_button.test.tsx.snap b/src/components/filter_group/__snapshots__/filter_button.test.tsx.snap index 25d9224e42f..53ae7242be7 100644 --- a/src/components/filter_group/__snapshots__/filter_button.test.tsx.snap +++ b/src/components/filter_group/__snapshots__/filter_button.test.tsx.snap @@ -42,6 +42,25 @@ exports[`EuiFilterButton is rendered 1`] = ` `; +exports[`EuiFilterButton props badgeColor is rendered 1`] = ` + +`; + exports[`EuiFilterButton props grow can be turned off 1`] = `