diff --git a/packages/ocean-core/src/components/_all.scss b/packages/ocean-core/src/components/_all.scss index 5cf46295c..72adb9fe6 100644 --- a/packages/ocean-core/src/components/_all.scss +++ b/packages/ocean-core/src/components/_all.scss @@ -41,5 +41,6 @@ @import 'list-settings'; @import 'list-expandable'; @import 'list-selectable'; +@import 'corner-tag'; @import 'internal-contextual-hero'; @import 'contextual-menu'; diff --git a/packages/ocean-core/src/components/_corner-tag.scss b/packages/ocean-core/src/components/_corner-tag.scss new file mode 100644 index 000000000..9c1b42a7f --- /dev/null +++ b/packages/ocean-core/src/components/_corner-tag.scss @@ -0,0 +1,28 @@ +.ods-corner-tag { + align-items: center; + border-radius: 0 0 0 $border-radius-sm; + color: $color-interface-light-pure; + display: inline-flex; + font-family: $font-family-base; + // No 10px token in @useblu/ocean-tokens — smallest is $font-size-xxxs (12px). + // Hardcoded literal follows precedent in _tag.scss and _badge.scss. + font-size: 10px; + font-weight: $font-weight-extrabold; + height: 20px; + line-height: 100%; + justify-content: flex-end; + padding: 0 $spacing-inline-xxs; + position: absolute; + right: 0; + text-align: right; + top: 0; + z-index: 2; + + &--primaryDown { + background-color: $color-brand-primary-down; + } + + &--complementaryPure { + background-color: $color-complementary-pure; + } +} diff --git a/packages/ocean-react/src/CornerTag/CornerTag.tsx b/packages/ocean-react/src/CornerTag/CornerTag.tsx new file mode 100644 index 000000000..dee7d4d9b --- /dev/null +++ b/packages/ocean-react/src/CornerTag/CornerTag.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import classNames from 'classnames'; + +export type CornerTagColor = 'primaryDown' | 'complementaryPure'; + +export interface CornerTagProps { + /** Text displayed inside the tag. Empty strings render nothing. */ + label: string; + /** Background color variant. Defaults to `primaryDown`. */ + color?: CornerTagColor; + /** Additional class names applied to the root element. */ + className?: string; +} + +const CornerTag = React.forwardRef( + ({ label, color = 'primaryDown', className }, ref) => { + if (!label) return null; + + return ( + + {label} + + ); + } +); + +CornerTag.displayName = 'CornerTag'; + +export default CornerTag; diff --git a/packages/ocean-react/src/CornerTag/__tests__/CornerTag.test.tsx b/packages/ocean-react/src/CornerTag/__tests__/CornerTag.test.tsx new file mode 100644 index 000000000..06459c43a --- /dev/null +++ b/packages/ocean-react/src/CornerTag/__tests__/CornerTag.test.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import CornerTag from '../CornerTag'; + +test('renders with default color (primaryDown)', () => { + render(); + + const tag = screen.getByText('Recomendado'); + expect(tag).toHaveClass('ods-corner-tag'); + expect(tag).toHaveClass('ods-corner-tag--primaryDown'); + expect(tag).toHaveAttribute('aria-label', 'Recomendado'); +}); + +test('renders with complementaryPure color', () => { + render(); + + const tag = screen.getByText('Novo'); + expect(tag).toHaveClass('ods-corner-tag--complementaryPure'); +}); + +test('does not render when label is empty', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); +}); + +test('forwards additional class names', () => { + render(); + + expect(screen.getByText('Promo')).toHaveClass('custom-class'); +}); + +test('expands without truncation for long labels', () => { + const longLabel = 'Texto muito longo que pode quebrar a linha'; + render(); + + const tag = screen.getByText(longLabel); + expect(tag).toHaveTextContent(longLabel); + expect(tag).toHaveAttribute('aria-label', longLabel); +}); diff --git a/packages/ocean-react/src/CornerTag/index.ts b/packages/ocean-react/src/CornerTag/index.ts new file mode 100644 index 000000000..807cc20b2 --- /dev/null +++ b/packages/ocean-react/src/CornerTag/index.ts @@ -0,0 +1,2 @@ +export { default } from './CornerTag'; +export type { CornerTagProps, CornerTagColor } from './CornerTag'; diff --git a/packages/ocean-react/src/ListReadOnly/ListReadOnly.tsx b/packages/ocean-react/src/ListReadOnly/ListReadOnly.tsx index 169f83714..83a2af904 100644 --- a/packages/ocean-react/src/ListReadOnly/ListReadOnly.tsx +++ b/packages/ocean-react/src/ListReadOnly/ListReadOnly.tsx @@ -7,6 +7,7 @@ import SkeletonBar from '../_shared/components/SkeletonBar'; import ListContainer, { ListContainerHighlight, } from '../_shared/components/ListContainer'; +import { CornerTagProps } from '../CornerTag/CornerTag'; export type ListReadOnlyProps = { /** @@ -67,6 +68,11 @@ export type ListReadOnlyProps = { * Renders a highlighted caption area at the bottom of the container. */ highlight?: ListContainerHighlight; + /** + * Renders a Highlight Corner Tag at the top-right corner of the card. + * Only rendered when `type='card'`. + */ + cornerTag?: CornerTagProps; } & React.ComponentPropsWithoutRef<'div'>; const ListReadOnly = React.forwardRef( @@ -86,6 +92,7 @@ const ListReadOnly = React.forwardRef( className, showDivider = false, highlight, + cornerTag, ...rest }, ref @@ -135,6 +142,7 @@ const ListReadOnly = React.forwardRef( type={type} showDivider={showDivider} highlight={highlight} + cornerTag={cornerTag} >