Skip to content
Merged
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
10 changes: 10 additions & 0 deletions renderers/angular/src/v0_8/components/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ describe('Image Component', () => {
expect(sectionEl.nativeElement.className).toContain('image-all-class');
});

it('should render <img> with altText if provided', () => {
fixture.componentRef.setInput('url', { literalString: 'http://example.com/a.png' });
fixture.componentRef.setInput('altText', { literalString: 'A beautiful sunset' });
fixture.detectChanges();

const imgEl = fixture.debugElement.query(By.css('img'));
expect(imgEl).toBeTruthy();
expect(imgEl.nativeElement.alt).toBe('A beautiful sunset');
});

it('should NOT render <img> if url is null', () => {
fixture.componentRef.setInput('usageHint', null);
fixture.detectChanges();
Expand Down
5 changes: 4 additions & 1 deletion renderers/angular/src/v0_8/components/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ import { DynamicComponent } from '../rendering/dynamic-component';
`,
template: `
@let resolvedUrl = this.resolvedUrl();
@let resolvedAltText = this.resolvedAltText();

@if (resolvedUrl) {
<section [class]="classes()" [style]="theme.additionalStyles?.Image">
<img [src]="resolvedUrl" alt="" />
<img [src]="resolvedUrl" [alt]="resolvedAltText" />
</section>
}
`,
Expand All @@ -52,8 +53,10 @@ export class Image extends DynamicComponent<Types.ImageNode> {
readonly url = input<Primitives.StringValue | null>(null);
readonly usageHint = input<Types.ResolvedImage['usageHint'] | null>(null);
readonly fit = input<Types.ResolvedImage['fit'] | null>(null);
readonly altText = input<Primitives.StringValue | null>(null);

protected readonly resolvedUrl = computed(() => this.resolvePrimitive(this.url()));
protected readonly resolvedAltText = computed(() => this.resolvePrimitive(this.altText()) || '');

protected classes = computed(() => {
const usageHint = this.usageHint();
Expand Down
6 changes: 4 additions & 2 deletions renderers/angular/src/v0_8/components/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { DynamicComponent } from '../rendering/dynamic-component';
selector: 'a2ui-slider',
template: `
<div [class]="theme.components.Slider.container" [style]="theme.additionalStyles?.Slider">
<label [class]="theme.components.Slider.label" [id]="labelId">{{ resolvedLabel() }}</label>
@if (resolvedLabel()) {
<label [class]="theme.components.Slider.label" [id]="labelId">{{ resolvedLabel() }}</label>
}
<input
type="range"
[class]="theme.components.Slider.element"
Expand All @@ -43,7 +45,7 @@ import { DynamicComponent } from '../rendering/dynamic-component';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Slider extends DynamicComponent<Types.SliderNode> {
readonly label = input.required<Types.StringValue | null>();
readonly label = input<Types.StringValue | null>(null);
readonly value = input.required<Types.NumberValue | null>();
readonly minValue = input<number>(0);
readonly maxValue = input<number>(100);
Expand Down
10 changes: 10 additions & 0 deletions renderers/angular/src/v0_9/catalog/basic/simple-components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ describe('Simple Components', () => {
expect(img.style.objectFit).toBe('cover');
expect(img.className).toContain('avatar');
});

it('should render image with description', () => {
fixture.componentRef.setInput('props', {
url: createBoundProperty('https://example.com/image.png'),
description: createBoundProperty('A cute cat'),
});
fixture.detectChanges();
const img = fixture.nativeElement.querySelector('img') as HTMLImageElement;
expect(img.alt).toBe('A cute cat');
});
});

describe('IconComponent', () => {
Expand Down
26 changes: 25 additions & 1 deletion renderers/lit/src/0.8/ui/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export class Image extends Root {
@property()
accessor url: Primitives.StringValue | null = null;

@property()
accessor altText: Primitives.StringValue | null = null;

@property()
accessor usageHint: Types.ResolvedImage["usageHint"] | null = null;

Expand Down Expand Up @@ -65,7 +68,28 @@ export class Image extends Root {
}

const render = (url: string) => {
return html`<img src=${url} />`;
let resolvedAlt = "";
if (this.altText) {
if (typeof this.altText === "object") {
if ("literalString" in this.altText) {
resolvedAlt = this.altText.literalString ?? "";
} else if ("literal" in this.altText) {
resolvedAlt = this.altText.literal ?? "";
} else if ("path" in this.altText && this.altText.path) {
if (this.processor && this.component) {
const data = this.processor.getData(
this.component,
this.altText.path,
this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID
);
if (typeof data === "string") {
resolvedAlt = data;
}
}
}
}
}
return html`<img src=${url} alt=${resolvedAlt} />`;
};

if (this.url && typeof this.url === "object") {
Expand Down
13 changes: 10 additions & 3 deletions renderers/lit/src/0.8/ui/slider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,16 @@ export class Slider extends Root {
return html`<section
class=${classMap(this.theme.components.Slider.container)}
>
<label class=${classMap(this.theme.components.Slider.label)} for="data">
${this.label?.literalString ?? ""}
</label>
${this.label
? html`<label class=${classMap(this.theme.components.Slider.label)} for="data">
${extractStringValue(
this.label,
this.component,
this.processor,
this.surfaceId
)}
</label>`
: nothing}
<input
autocomplete="off"
class=${classMap(this.theme.components.Slider.element)}
Expand Down
1 change: 1 addition & 0 deletions renderers/lit/src/v0_9/catalogs/basic/components/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class A2uiImageElement extends A2uiLitElement<typeof ImageApi> {
const styles = { objectFit: props.fit || "fill", width: "100%" };
return html`<img
src=${props.url}
alt=${props.description || ""}
class=${"a2ui-image " + (props.variant || "")}
style=${styleMap(styles)}
/>`;
Expand Down
17 changes: 5 additions & 12 deletions renderers/react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions renderers/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,11 @@
"@a2ui/web_core": "file:../web_core",
"clsx": "^2.1.0",
"markdown-it": "^14.0.0",
"zod": "^3.23.8",
"rxjs": "^7.8.1"
"zod": "^3.23.8"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"rxjs": "^7.8.1",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
8 changes: 5 additions & 3 deletions renderers/react/src/v0_8/components/interactive/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export const Slider = memo(function Slider({
return (
<div className="a2ui-slider" style={hostStyle}>
<section className={classMapToString(theme.components.Slider.container)}>
<label htmlFor={id} className={classMapToString(theme.components.Slider.label)}>
{label}
</label>
{label && (
<label htmlFor={id} className={classMapToString(theme.components.Slider.label)}>
{label}
</label>
)}
<input
type="range"
id={id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ export const Image = createReactComponent(ImageApi, ({props}) => {
style.objectFit = 'cover';
}

return <img src={props.url} alt="" style={style} />;
return <img src={props.url} alt={props.description || ''} style={style} />;
});
21 changes: 21 additions & 0 deletions renderers/react/tests/v0_8/unit/components/Slider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ describe('Slider Component', () => {
it('should have correct DOM structure: label, input, span in order', () => {
const messages = createSimpleMessages('sl-1', 'Slider', {
value: { literalNumber: 50 },
label: { literalString: 'Volume' },
minValue: 0,
maxValue: 100,
});
Expand All @@ -305,6 +306,26 @@ describe('Slider Component', () => {
expect(children[2]?.tagName).toBe('SPAN');
});

it('should omit label from DOM structure when not provided', () => {
const messages = createSimpleMessages('sl-1', 'Slider', {
value: { literalNumber: 50 },
});

const { container } = render(
<TestWrapper>
<TestRenderer messages={messages} />
</TestWrapper>
);

const section = container.querySelector('section');
expect(section).toBeInTheDocument();

const children = Array.from(section?.children ?? []);
expect(children.length).toBe(2);
expect(children[0]?.tagName).toBe('INPUT');
expect(children[1]?.tagName).toBe('SPAN');
});

it('should have input inside section container', () => {
const messages = createSimpleMessages('sl-1', 'Slider', {
value: { literalNumber: 50 },
Expand Down
9 changes: 9 additions & 0 deletions renderers/react/tests/v0_9/basic_catalog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ describe('Basic Catalog Components', () => {
expect(img.style.objectFit).toBe('cover');
});

it('renders image with description as alt text', () => {
const { view } = renderA2uiComponent(Image, 'i1', {
url: 'url',
description: 'A beautiful sunset'
});
const img = view.container.querySelector('img') as HTMLImageElement;
expect(img.alt).toBe('A beautiful sunset');
});

it('applies variant-specific styling (avatar)', () => {
const { view } = renderA2uiComponent(Image, 'i1', {
url: 'url',
Expand Down
1 change: 1 addition & 0 deletions renderers/web_core/src/v0_8/schema/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export const SliderSchema = z.object({
.superRefine(exactlyOneKey),
minValue: z.number().optional(),
maxValue: z.number().optional(),
label: StringValueSchema.optional(),
});

export const ComponentArrayTemplateSchema = z.object({
Expand Down
Loading