Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ocean-core/src/components/_all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@
@import 'list-settings';
@import 'list-expandable';
@import 'list-selectable';
@import 'corner-tag';
@import 'internal-contextual-hero';
@import 'contextual-menu';
28 changes: 28 additions & 0 deletions packages/ocean-core/src/components/_corner-tag.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
37 changes: 37 additions & 0 deletions packages/ocean-react/src/CornerTag/CornerTag.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSpanElement, CornerTagProps>(
({ label, color = 'primaryDown', className }, ref) => {
if (!label) return null;

return (
<span
ref={ref}
className={classNames(
'ods-corner-tag',
`ods-corner-tag--${color}`,
className
)}
aria-label={label}
>
{label}
</span>
);
}
);

CornerTag.displayName = 'CornerTag';

export default CornerTag;
41 changes: 41 additions & 0 deletions packages/ocean-react/src/CornerTag/__tests__/CornerTag.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<CornerTag label="Recomendado" />);

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(<CornerTag label="Novo" color="complementaryPure" />);

const tag = screen.getByText('Novo');
expect(tag).toHaveClass('ods-corner-tag--complementaryPure');
});

test('does not render when label is empty', () => {
const { container } = render(<CornerTag label="" />);

expect(container).toBeEmptyDOMElement();
});

test('forwards additional class names', () => {
render(<CornerTag label="Promo" className="custom-class" />);

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(<CornerTag label={longLabel} />);

const tag = screen.getByText(longLabel);
expect(tag).toHaveTextContent(longLabel);
expect(tag).toHaveAttribute('aria-label', longLabel);
});
2 changes: 2 additions & 0 deletions packages/ocean-react/src/CornerTag/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './CornerTag';
export type { CornerTagProps, CornerTagColor } from './CornerTag';
8 changes: 8 additions & 0 deletions packages/ocean-react/src/ListReadOnly/ListReadOnly.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -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<HTMLDivElement, ListReadOnlyProps>(
Expand All @@ -86,6 +92,7 @@ const ListReadOnly = React.forwardRef<HTMLDivElement, ListReadOnlyProps>(
className,
showDivider = false,
highlight,
cornerTag,
...rest
},
ref
Expand Down Expand Up @@ -135,6 +142,7 @@ const ListReadOnly = React.forwardRef<HTMLDivElement, ListReadOnlyProps>(
type={type}
showDivider={showDivider}
highlight={highlight}
cornerTag={cornerTag}
>
<div
ref={ref}
Expand Down
35 changes: 27 additions & 8 deletions packages/ocean-react/src/ListSelectable/ListSelectable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import ListReadOnly from '../ListReadOnly/ListReadOnly';
import ListContainer, {
ListContainerHighlight,
} from '../_shared/components/ListContainer';
import { CornerTagProps } from '../CornerTag/CornerTag';

interface ListSelectableProps {
interface ListSelectableBaseProps {
/** Required main title displayed on the list item. */
title: string;
/** Optional secondary text rendered below the title. */
Expand All @@ -38,8 +39,6 @@ interface ListSelectableProps {
indicator?: ReactNode;
/** Visual state applied to the text content (`default`, `warning`, etc.). */
status?: ContentListProps['type'];
/** Layout style of the wrapper: `text` keeps inline divider, `card` shows borders. */
type?: 'card' | 'text';
/** Platform context used to adjust spacing (web or app). */
platform?: 'web' | 'app';
/** If the selectable is disabled, the input will be hidden and the content will be rendered as ListReadOnly. */
Expand All @@ -48,9 +47,27 @@ interface ListSelectableProps {
highlight?: ListContainerHighlight;
}

type ListSelectableCardProps = ListSelectableBaseProps & {
/** Layout style of the wrapper: `text` keeps inline divider, `card` shows borders. */
type?: 'card';
/**
* Renders a Highlight Corner Tag at the top-right corner of the card.
* Only available when `type='card'`.
*/
cornerTag?: CornerTagProps;
};

type ListSelectableTextProps = ListSelectableBaseProps & {
type: 'text';
/** Corner Tag is not supported in `type='text'` (no visual container to anchor it). */
cornerTag?: never;
};

type ListSelectableProps = ListSelectableCardProps | ListSelectableTextProps;

const ListSelectable = React.forwardRef<HTMLDivElement, ListSelectableProps>(
(
{
(props, ref) => {
const {
title,
description,
caption,
Expand All @@ -68,10 +85,10 @@ const ListSelectable = React.forwardRef<HTMLDivElement, ListSelectableProps>(
status = 'default',
type = 'card',
platform = 'web',
cornerTag: _cornerTag,
...rest
},
ref
) => {
} = props as ListSelectableCardProps;
const cornerTag = type === 'card' ? _cornerTag : undefined;
const hasError = useMemo(
() => radio?.error || checkbox?.error,
[radio?.error, checkbox?.error]
Expand Down Expand Up @@ -141,6 +158,7 @@ const ListSelectable = React.forwardRef<HTMLDivElement, ListSelectableProps>(
caption={caption}
strikethroughDescription={strikethroughDescription}
highlight={highlight}
cornerTag={cornerTag}
{...rest}
ref={ref}
/>
Expand All @@ -153,6 +171,7 @@ const ListSelectable = React.forwardRef<HTMLDivElement, ListSelectableProps>(
showDivider={showDivider}
hasError={hasError}
highlight={highlight}
cornerTag={cornerTag}
>
<div
className={classNames('ods-list-selectable', className, {
Expand Down
Loading
Loading