Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2b569d9

Browse files
committed
feat(core): render ARIA property bindings as attributes
Allow binding to ARIA attributes using property binding syntax _without_the `attr.` prefix. For example, `[aria-label]="expr"` is now valid, andequivalent to `[ariaLabel]="expr"`. Both examples bind to either amatching input or the `aria-label` HTML attribute, rather than the`ariaLabel` DOM property.Binding ARIA properties as attributes will ensure they are renderedcorrectly on the server, where the emulated DOM may not correctlyreflect ARIA properties as attributes.
1 parent6adaf01 commit2b569d9

File tree

9 files changed

+239
-16
lines changed

9 files changed

+239
-16
lines changed

‎packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export class Identifiers {
252252

253253
staticdomProperty:o.ExternalReference={name:'ɵɵdomProperty',moduleName:CORE};
254254

255+
staticariaProperty:o.ExternalReference={name:'ɵɵariaProperty',moduleName:CORE};
255256
staticproperty:o.ExternalReference={name:'ɵɵproperty',moduleName:CORE};
256257

257258
statici18n:o.ExternalReference={name:'ɵɵi18n',moduleName:CORE};

‎packages/compiler/src/template/pipeline/src/instruction.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,14 @@ export function i18nAttributes(slot: number, i18nAttributesConfig: number): ir.C
582582
returncall(Identifiers.i18nAttributes,args,null);
583583
}
584584

585+
exportfunctionariaProperty(
586+
name:string,
587+
expression:o.Expression|ir.Interpolation,
588+
sourceSpan:ParseSourceSpan,
589+
):ir.UpdateOp{
590+
returnpropertyBase(Identifiers.ariaProperty,name,expression,null,sourceSpan);
591+
}
592+
585593
exportfunctionproperty(
586594
name:string,
587595
expression:o.Expression|ir.Interpolation,

‎packages/compiler/src/template/pipeline/src/phases/reify.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
}from'../compilation';
1818
import*asngfrom'../instruction';
1919

20+
constARIA_PREFIX='aria';
21+
2022
/**
2123
* Map of target resolvers for event listeners.
2224
*/
@@ -562,13 +564,8 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
562564
ir.OpList.replace(
563565
op,
564566
unit.job.mode===TemplateCompilationMode.DomOnly&&!op.isLegacyAnimationTrigger
565-
?ng.domProperty(
566-
DOM_PROPERTY_REMAPPING.get(op.name)??op.name,
567-
op.expression,
568-
op.sanitizer,
569-
op.sourceSpan,
570-
)
571-
:ng.property(op.name,op.expression,op.sanitizer,op.sourceSpan),
567+
?reifyDomProperty(op)
568+
:reifyProperty(op),
572569
);
573570
break;
574571
caseir.OpKind.TwoWayProperty:
@@ -614,15 +611,7 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
614611
if(op.isLegacyAnimationTrigger){
615612
ir.OpList.replace(op,ng.syntheticHostProperty(op.name,op.expression,op.sourceSpan));
616613
}else{
617-
ir.OpList.replace(
618-
op,
619-
ng.domProperty(
620-
DOM_PROPERTY_REMAPPING.get(op.name)??op.name,
621-
op.expression,
622-
op.sanitizer,
623-
op.sourceSpan,
624-
),
625-
);
614+
ir.OpList.replace(op,reifyDomProperty(op));
626615
}
627616
}
628617
break;
@@ -662,6 +651,28 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
662651
}
663652
}
664653

654+
functionariaAttrName(name:string):string{
655+
// Convert an ARIA property name to its corresponding attribute name, if necessary.
656+
returnname.charAt(4)!=='-' ?`${ARIA_PREFIX}-${name.slice(4).toLowerCase()}` :name;
657+
}
658+
659+
functionreifyDomProperty(op:ir.DomPropertyOp|ir.PropertyOp):ir.UpdateOp{
660+
returnop.name.startsWith(ARIA_PREFIX)
661+
?ng.attribute(ariaAttrName(op.name),op.expression,null,null,op.sourceSpan)
662+
:ng.domProperty(
663+
DOM_PROPERTY_REMAPPING.get(op.name)??op.name,
664+
op.expression,
665+
op.sanitizer,
666+
op.sourceSpan,
667+
);
668+
}
669+
670+
functionreifyProperty(op:ir.PropertyOp):ir.UpdateOp{
671+
returnop.name.startsWith(ARIA_PREFIX)
672+
?ng.ariaProperty(op.name,op.expression,op.sourceSpan)
673+
:ng.property(op.name,op.expression,op.sanitizer,op.sourceSpan);
674+
}
675+
665676
functionreifyIrExpression(expr:o.Expression):o.Expression{
666677
if(!ir.isIrExpression(expr)){
667678
returnexpr;

‎packages/core/src/render3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export {ɵɵgetInheritedFactory} from './di';
5353
export{getLocaleId,setLocaleId}from'./i18n/i18n_locale_id';
5454
export{
5555
ɵɵadvance,
56+
ɵɵariaProperty,
5657
ɵɵattribute,
5758
ɵɵinterpolate,
5859
ɵɵinterpolate1,

‎packages/core/src/render3/instructions/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
*/
2828
export*from'../../defer/instructions';
2929
export*from'./advance';
30+
export*from'./aria_property';
3031
export*from'./attribute';
3132
export*from'./change_detection';
3233
export*from'./component_instance';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
*@license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
import{bindingUpdated}from'../bindings';
9+
import{RENDERER}from'../interfaces/view';
10+
import{getLView,getSelectedTNode,getTView,nextBindingIndex}from'../state';
11+
import{setAriaAttributeAndInputs,storePropertyBindingMetadata}from'./shared';
12+
13+
/**
14+
* Update an ARIA attribute by either its attribute or property name on a selected element.
15+
*
16+
* If the property name also exists as an input property on any of the element's directives, those
17+
* inputs will be set instead of the element property.
18+
*
19+
*@param name Name of the ARIA attribute or property (beginning with `aria`).
20+
*@param value New value to write.
21+
*@returns This function returns itself so that it may be chained.
22+
*
23+
*@codeGenApi
24+
*/
25+
exportfunctionɵɵariaProperty<T>(name:string,value:T):typeofɵɵariaProperty{
26+
constlView=getLView();
27+
constbindingIndex=nextBindingIndex();
28+
if(bindingUpdated(lView,bindingIndex,value)){
29+
consttView=getTView();
30+
consttNode=getSelectedTNode();
31+
setAriaAttributeAndInputs(tNode,lView,name,value,lView[RENDERER]);
32+
ngDevMode&&storePropertyBindingMetadata(tView.data,tNode,name,bindingIndex);
33+
}
34+
returnɵɵariaProperty;
35+
}

‎packages/core/src/render3/jit/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({
8484
'ɵɵpipeBindV':r3.ɵɵpipeBindV,
8585
'ɵɵprojectionDef':r3.ɵɵprojectionDef,
8686
'ɵɵdomProperty':r3.ɵɵdomProperty,
87+
'ɵɵariaProperty':r3.ɵɵariaProperty,
8788
'ɵɵproperty':r3.ɵɵproperty,
8889
'ɵɵpipe':r3.ɵɵpipe,
8990
'ɵɵqueryRefresh':r3.ɵɵqueryRefresh,

‎packages/core/test/acceptance/property_binding_spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,117 @@ describe('property bindings', () => {
129129
},
130130
);
131131

132+
it('should bind ARIA properties to their corresponding attributes',()=>{
133+
@Component({
134+
template:'<button [ariaLabel]="label"></button>',
135+
})
136+
classMyComp{
137+
label?:string;
138+
}
139+
140+
constfixture=TestBed.createComponent(MyComp);
141+
constbutton=fixture.debugElement.query(By.css('button')).nativeElement;
142+
143+
fixture.componentInstance.label='Open';
144+
fixture.detectChanges();
145+
146+
expect(button.getAttribute('aria-label')).toBe('Open');
147+
148+
fixture.componentInstance.label='Close';
149+
fixture.detectChanges();
150+
151+
expect(button.getAttribute('aria-label')).toBe('Close');
152+
});
153+
154+
describe('should bind to ARIA attribute names',()=>{
155+
it('on HTML elements',()=>{
156+
@Component({
157+
template:'<button [aria-label]="label"></button>',
158+
})
159+
classMyComp{
160+
label?:string;
161+
}
162+
163+
constfixture=TestBed.createComponent(MyComp);
164+
constbutton=fixture.debugElement.query(By.css('button')).nativeElement;
165+
166+
fixture.componentInstance.label='Open';
167+
fixture.detectChanges();
168+
169+
expect(button.getAttribute('aria-label')).toBe('Open');
170+
171+
fixture.componentInstance.label='Close';
172+
fixture.detectChanges();
173+
174+
expect(button.getAttribute('aria-label')).toBe('Close');
175+
});
176+
177+
it('on component elements',()=>{
178+
@Component({
179+
selector:'button[fancy]',
180+
})
181+
classFancyButton{}
182+
183+
@Component({
184+
template:'<button fancy [aria-label]="label"></button>',
185+
imports:[FancyButton],
186+
})
187+
classMyComp{
188+
label?:string;
189+
}
190+
191+
constfixture=TestBed.createComponent(MyComp);
192+
constbutton=fixture.debugElement.query(By.css('button')).nativeElement;
193+
194+
fixture.componentInstance.label='Open';
195+
fixture.detectChanges();
196+
197+
expect(button.getAttribute('aria-label')).toBe('Open');
198+
199+
fixture.componentInstance.label='Close';
200+
fixture.detectChanges();
201+
202+
expect(button.getAttribute('aria-label')).toBe('Close');
203+
});
204+
});
205+
206+
it(
207+
'should not bind to ARIA properties by their corresponding attribute names, if they '+
208+
'correspond to inputs',
209+
()=>{
210+
@Component({
211+
template:'',
212+
selector:'my-comp',
213+
})
214+
classMyComp{
215+
@Input({alias:'aria-label'})myAriaLabel?:string;
216+
}
217+
218+
@Component({
219+
template:'<my-comp [aria-label]="label"></my-comp>',
220+
imports:[MyComp],
221+
})
222+
classApp{
223+
label='a';
224+
}
225+
226+
constfixture=TestBed.createComponent(App);
227+
constmyCompNode=fixture.debugElement.query(By.directive(MyComp));
228+
229+
fixture.componentInstance.label='a';
230+
fixture.detectChanges();
231+
232+
expect(myCompNode.nativeElement.getAttribute('aria-label')).toBeFalsy();
233+
expect(myCompNode.componentInstance.myAriaLabel).toBe('a');
234+
235+
fixture.componentInstance.label='b';
236+
fixture.detectChanges();
237+
238+
expect(myCompNode.nativeElement.getAttribute('aria-label')).toBeFalsy();
239+
expect(myCompNode.componentInstance.myAriaLabel).toBe('b');
240+
},
241+
);
242+
132243
it('should use the sanitizer in bound properties',()=>{
133244
@Component({
134245
template:`
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
*@license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import{Component}from'@angular/core';
10+
import{ssr}from'./hydration_utils';
11+
12+
describe('renderApplication',()=>{
13+
it('should render ARIA attributes from attribute bindings',async()=>{
14+
@Component({
15+
selector:'app',
16+
standalone:true,
17+
template:'<div [attr.aria-label]="label"></div>',
18+
})
19+
classSomeComponent{
20+
label='some label';
21+
}
22+
23+
consthtml=awaitssr(SomeComponent);
24+
expect(html).toContain('aria-label="some label"');
25+
});
26+
27+
it('should render ARIA attributes from their corresponding property bindings',async()=>{
28+
@Component({
29+
selector:'app',
30+
standalone:true,
31+
template:'<div [ariaLabel]="label"></div>',
32+
})
33+
classSomeComponent{
34+
label='some other label';
35+
}
36+
37+
consthtml=awaitssr(SomeComponent);
38+
expect(html).toContain('aria-label="some other label"');
39+
});
40+
41+
it('should render ARIA attributes using property binding syntax',async()=>{
42+
@Component({
43+
selector:'app',
44+
standalone:true,
45+
template:'<div [aria-label]="label"></div>',
46+
})
47+
classSomeComponent{
48+
label='a third label';
49+
}
50+
51+
consthtml=awaitssr(SomeComponent);
52+
expect(html).toContain('aria-label="a third label"');
53+
});
54+
});

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp