diff --git a/application/ui/src/features/annotator/annotations/annotation-labels.component.test.tsx b/application/ui/src/features/annotator/annotations/annotation-labels.component.test.tsx
new file mode 100644
index 0000000000..d6f7909d9e
--- /dev/null
+++ b/application/ui/src/features/annotator/annotations/annotation-labels.component.test.tsx
@@ -0,0 +1,136 @@
+// Copyright (C) 2025 Intel Corporation
+// SPDX-License-Identifier: Apache-2.0
+
+import { getMockedLabel } from 'mocks/mock-labels';
+import { fireEvent, render, screen } from 'test-utils/render';
+import { describe, expect, it, vi } from 'vitest';
+
+import { Label } from '../types';
+import { AnnotationLabels } from './annotation-labels.component';
+
+const mockZoom = { scale: 1, maxZoomIn: 10, hasAnimation: false, translate: { x: 0, y: 0 } };
+
+vi.mock('src/components/zoom/zoom.provider', () => ({
+ useZoom: () => mockZoom,
+}));
+
+describe('AnnotationLabels', () => {
+ const mockOnRemove = vi.fn();
+
+ afterEach(() => {
+ mockOnRemove.mockClear();
+ });
+
+ it('renders placeholder when no labels provided', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('No label')).toBeInTheDocument();
+ });
+
+ it('renders single label with name and color', () => {
+ const label = getMockedLabel({ name: 'Person', color: '#FF0000' });
+
+ render(
+
+ );
+
+ expect(screen.getByText('Person')).toBeInTheDocument();
+
+ const rect = screen.getByLabelText('label Person background');
+ expect(rect).toHaveAttribute('fill', '#FF0000');
+ });
+
+ it('renders multiple labels horizontally', () => {
+ const labels: Label[] = [
+ getMockedLabel({ id: '1', name: 'Person', color: '#FF0000' }),
+ getMockedLabel({ id: '2', name: 'Car', color: '#00FF00' }),
+ ];
+
+ render(
+
+ );
+
+ expect(screen.getByText('Person')).toBeInTheDocument();
+ expect(screen.getByText('Car')).toBeInTheDocument();
+ });
+
+ it('calls onRemove when close button clicked', () => {
+ const label = getMockedLabel({ id: 'label-1', name: 'Person' });
+
+ render(
+
+ );
+
+ const closeButton = screen.getByLabelText('Remove Person');
+ fireEvent.pointerDown(closeButton);
+
+ expect(mockOnRemove).toHaveBeenCalledTimes(1);
+ expect(mockOnRemove).toHaveBeenCalledWith('label-1');
+ });
+
+ it('adjusts sizes based on zoom scale', () => {
+ mockZoom.scale = 2;
+ const label = getMockedLabel({ name: 'Person' });
+
+ render(
+
+ );
+
+ const text = screen.getByLabelText('label Person');
+
+ // Font size should be 14 / 2 = 7
+ expect(text).toHaveAttribute('font-size', '7');
+ });
+
+ it('prevents event propagation on close button click', () => {
+ const label = getMockedLabel({ id: 'label-1', name: 'Person' });
+ const mockParentHandler = vi.fn();
+
+ render(
+
+ );
+
+ const closeButton = screen.getByLabelText('Remove Person');
+ fireEvent.pointerDown(closeButton);
+
+ expect(mockOnRemove).toHaveBeenCalled();
+ // Parent handler should not be called due to stopPropagation
+ expect(mockParentHandler).not.toHaveBeenCalled();
+ });
+
+ it('renders labels with correct positioning (no overlap)', () => {
+ const labels: Label[] = [
+ getMockedLabel({ id: '1', name: 'A', color: '#FF0000' }),
+ getMockedLabel({ id: '2', name: 'B', color: '#00FF00' }),
+ ];
+
+ render(
+
+ );
+
+ const firstRect = screen.getByLabelText('label A background');
+ const secondRect = screen.getByLabelText('label B background');
+
+ const firstX = parseFloat(firstRect.getAttribute('x') || '0');
+ const secondX = parseFloat(secondRect.getAttribute('x') || '0');
+
+ // Second label should be positioned after the first
+ expect(secondX).toBeGreaterThan(firstX);
+ });
+});
diff --git a/application/ui/src/features/annotator/annotations/annotation-labels.component.tsx b/application/ui/src/features/annotator/annotations/annotation-labels.component.tsx
index f1976ae969..e4a5b20e37 100644
--- a/application/ui/src/features/annotator/annotations/annotation-labels.component.tsx
+++ b/application/ui/src/features/annotator/annotations/annotation-labels.component.tsx
@@ -3,6 +3,7 @@
import { PointerEvent, useCallback } from 'react';
+import { isEmpty } from 'lodash-es';
import { useZoom } from 'src/components/zoom/zoom.provider';
import { v4 as uuid } from 'uuid';
@@ -85,8 +86,15 @@ export const AnnotationLabels = ({ labels, onRemove }: AnnotationLabelsProps) =>
fill={placeholderLabel.color}
stroke='none'
rx={styles.borderRadius}
+ aria-label={`label ${placeholderLabel.name} background`}
/>
-
+
{placeholderLabel.name}
@@ -94,7 +102,7 @@ export const AnnotationLabels = ({ labels, onRemove }: AnnotationLabelsProps) =>
}
return labels.map((label) => {
- const labelWidth = calculateLabelWidth(label.name) + closeButtonWidth;
+ const labelWidth = !isEmpty(label.name) ? calculateLabelWidth(label.name) + closeButtonWidth : 0;
const xOffset = fullLengthOfAllLabels;
fullLengthOfAllLabels += labelWidth + gap;
@@ -110,12 +118,19 @@ export const AnnotationLabels = ({ labels, onRemove }: AnnotationLabelsProps) =>
fill={label.color}
stroke='none'
rx={styles.borderRadius}
+ aria-label={`label ${label.name} background`}
/>
-
+
{label.name}
- {/* Close button */}
+ {/* Remove button */}
y={yOffset + styles.textYOffset}
fontSize={fontSize}
fill='#fff'
+ aria-label={`Remove ${label.name}`}
>
x