@@ -2,6 +2,8 @@ import { consume } from '@lit/context';
22import {
33 θaddAdoptedStylesController as addAdoptedStylesController ,
44 θaddThemingController as addThemingController ,
5+ IgcButtonComponent ,
6+ IgcChipComponent ,
57 IgcDropdownComponent ,
68 type IgcDropdownItemComponent ,
79 type IgcIconComponent ,
@@ -11,11 +13,12 @@ import { html, LitElement, nothing, type PropertyValues } from 'lit';
1113import { property , query } from 'lit/decorators.js' ;
1214import { ifDefined } from 'lit/directives/if-defined.js' ;
1315import type { StateController } from '../controllers/state.js' ;
16+ import { addA11y , FILTER_ROW_INDEX } from '../internal/a11y.js' ;
1417import { DEFAULT_COLUMN_CONFIG } from '../internal/constants.js' ;
1518import { GRID_STATE_CONTEXT } from '../internal/context.js' ;
1619import { registerComponent } from '../internal/register.js' ;
1720import { GRID_FILTER_ROW_TAG } from '../internal/tags.js' ;
18- import type { ColumnConfiguration , PropertyType } from '../internal/types.js' ;
21+ import type { ColumnConfiguration , Keys , PropertyType } from '../internal/types.js' ;
1922import { getFilterOperandsFor } from '../internal/utils.js' ;
2023import { watch } from '../internal/watch.js' ;
2124import type { FilterExpressionTree } from '../operations/filter/tree.js' ;
@@ -26,6 +29,9 @@ import { all } from '../styles/themes/filtering-row-themes.js';
2629/** Number of filter expressions shown as chips before collapsing into a single counted chip. */
2730const MAX_PREVIEW_CHIPS = 3 ;
2831
32+ /** Accessible name of the filter row itself. */
33+ const FILTER_ROW_LABEL = 'Column filters' ;
34+
2935type ExpressionChipProps < T > = {
3036 expression : FilterExpression < T > ;
3137 selected : boolean ;
@@ -50,8 +56,18 @@ export default class IgcFilterRow<T extends object> extends LitElement {
5056
5157 public static override styles = styles ;
5258
59+ /**
60+ * Only the filter row renders these editor components, so the grid defers this
61+ * call until a filterable column shows one.
62+ */
5363 public static register ( ) {
54- registerComponent ( IgcFilterRow ) ;
64+ registerComponent (
65+ IgcFilterRow ,
66+ IgcButtonComponent ,
67+ IgcChipComponent ,
68+ IgcInputComponent ,
69+ IgcDropdownComponent
70+ ) ;
5571 }
5672
5773 private readonly _adoptedStylesController = addAdoptedStylesController ( this ) ;
@@ -96,6 +112,15 @@ export default class IgcFilterRow<T extends object> extends LitElement {
96112 constructor ( ) {
97113 super ( ) ;
98114
115+ // `role=grid` permits only rows and rowgroups as children, so a `search`
116+ // landmark here would be an illegal node in the grid tree. The filter row is
117+ // modeled as the second header row: one `gridcell` per column, each with that
118+ // column's filter controls.
119+ addA11y ( this , 'row' ) . set ( {
120+ ariaRowIndex : `${ FILTER_ROW_INDEX } ` ,
121+ ariaLabel : FILTER_ROW_LABEL ,
122+ } ) ;
123+
99124 addThemingController ( this , all , {
100125 themeChange : this . _handleThemeChange ,
101126 } ) ;
@@ -203,6 +228,25 @@ export default class IgcFilterRow<T extends object> extends LitElement {
203228 this . dropdown . toggle ( this . input ) ;
204229 }
205230
231+ #handleConditionKeydown( event : KeyboardEvent ) {
232+ if ( event . key !== 'Enter' && event . key !== ' ' ) {
233+ return ;
234+ }
235+
236+ // The trigger sits in the prefix slot of the input. The keystroke must not
237+ // reach the input and commit a filter behind the open dropdown.
238+ event . preventDefault ( ) ;
239+ event . stopPropagation ( ) ;
240+
241+ this . #openDropdownList( ) ;
242+ }
243+
244+ /** Header text of a column. Names the filter controls that act on it. */
245+ #nameFor( field : Keys < T > ) : string {
246+ const column = this . state . columns . find ( ( each ) => each . field === field ) ;
247+ return String ( column ?. header ?? field ) ;
248+ }
249+
206250 @watch ( 'active' , { waitUntilFirstUpdate : true } )
207251 protected activeChanged ( ) {
208252 this . style . display = this . active ? 'flex' : '' ;
@@ -211,7 +255,8 @@ export default class IgcFilterRow<T extends object> extends LitElement {
211255 this . column = DEFAULT_COLUMN_CONFIG as ColumnConfiguration < T > ;
212256 }
213257
214- this . state . host . requestUpdate ( ) ;
258+ // The header row marks the column whose filter editor is open.
259+ this . state . updateObservers ( ) ;
215260 }
216261
217262 #chipCriteriaFor( expression : FilterExpression < T > ) {
@@ -268,10 +313,18 @@ export default class IgcFilterRow<T extends object> extends LitElement {
268313
269314 const prefix = html `< span slot ="select "> </ span > ${ prefixedIcon ( name ) } ` ;
270315
316+ // The chip renders its select and remove actions in its own shadow root.
317+ // Resource strings are the only way to name them after the expression.
318+ const expression = `${ this . #nameFor( props . expression . key ) } ${ name } ${ unary ? '' : term } ` . trim ( ) ;
319+
271320 return html `
272321 < igc-chip
273322 selectable
274323 removable
324+ .resourceStrings =${ {
325+ chip_remove : `Remove filter ${ expression } ` ,
326+ chip_select : `Edit filter ${ expression } ` ,
327+ } }
275328 ?selected =${ props . selected }
276329 @igcRemove=${ props . onRemove }
277330 @igcSelect=${ props . onSelect }
@@ -341,22 +394,31 @@ export default class IgcFilterRow<T extends object> extends LitElement {
341394 }
342395
343396 protected renderDropdownTarget ( ) {
397+ const condition = this . condition . label ?? this . condition . name ;
398+
344399 return html `< igc-icon
345400 id ="condition "
346401 slot ="prefix "
347402 collection ="internal "
403+ role ="button "
404+ tabindex ="0 "
405+ aria-haspopup ="listbox "
406+ aria-label =${ `Filter condition: ${ condition } ` }
348407 .name =${ this . condition . name }
349408 @click=${ this . #openDropdownList}
409+ @keydown=${ this . #handleConditionKeydown}
350410 >
351411 </ igc-icon > ` ;
352412 }
353413
354414 protected renderInputArea ( ) {
415+ // `igc-input` names its inner control from a slotted label or the placeholder
416+ // only, so the placeholder carries the column name.
355417 return html `
356418 < igc-input
357419 outlined
358420 value =${ ifDefined ( this . expression . searchTerm ) }
359- placeholder ="Add filter value"
421+ placeholder =${ `Filter ${ this . #nameFor ( this . column . field ) } ` }
360422 ?readonly=${ this . condition . unary }
361423 @igcInput=${ this . #handleInput}
362424 @keydown=${ this . #handleKeydown}
@@ -369,7 +431,10 @@ export default class IgcFilterRow<T extends object> extends LitElement {
369431
370432 protected renderActiveState ( ) {
371433 return html `
372- < div part ="active-state ">
434+ < div
435+ part ="active-state "
436+ role ="gridcell "
437+ >
373438 < div part ="filter-row-input "> ${ this . renderInputArea ( ) } </ div >
374439 < div part ="filter-row-filters "> ${ this . renderActiveChips ( ) } </ div >
375440 < div part ="filter-row-actions "> ${ this . renderFilterActions ( ) } </ div >
@@ -410,6 +475,7 @@ export default class IgcFilterRow<T extends object> extends LitElement {
410475 const count = hidden ? html `< span slot ="suffix "> ${ state . length } </ span > ` : nothing ;
411476 const chip = html `< igc-chip
412477 data-column =${ column . field }
478+ aria-label =${ `Filter ${ this . #nameFor( column . field ) } ` }
413479 @click=${ open }
414480 > ${ prefixedIcon ( 'filter' ) } Filter${ count } </ igc-chip
415481 > ` ;
@@ -418,15 +484,19 @@ export default class IgcFilterRow<T extends object> extends LitElement {
418484 }
419485
420486 protected renderInactiveState ( ) {
421- return this . state . columns . map ( ( column ) =>
422- column . hidden
423- ? nothing
424- : html `
425- < div part ="filter-row-preview ">
426- ${ column . filterable ? this . renderFilterState ( column ) : nothing }
427- </ div >
428- `
429- ) ;
487+ return this . state . columns
488+ . filter ( ( column ) => ! column . hidden )
489+ . map (
490+ ( column , index ) => html `
491+ < div
492+ part ="filter-row-preview "
493+ role ="gridcell "
494+ aria-colindex =${ index + 1 }
495+ >
496+ ${ column . filterable ? this . renderFilterState ( column ) : nothing }
497+ </ div >
498+ `
499+ ) ;
430500 }
431501
432502 protected override render ( ) {
0 commit comments