@@ -11,17 +11,25 @@ import {
1111 afterRenderEffect ,
1212 booleanAttribute ,
1313 computed ,
14- contentChild ,
1514 contentChildren ,
1615 Directive ,
1716 ElementRef ,
1817 inject ,
1918 input ,
19+ output ,
2020 model ,
2121 Signal ,
2222} from '@angular/core' ;
2323import { Directionality } from '@angular/cdk/bidi' ;
24- import { GridPattern , GridRowPattern , GridCellPattern , GridCellWidgetPattern } from '../private' ;
24+ import {
25+ GridPattern ,
26+ GridRowPattern ,
27+ GridCellPattern ,
28+ GridCellWidgetPattern ,
29+ NavigateEvent ,
30+ } from '../private' ;
31+
32+ export { NavigateEvent } from '../private' ;
2533
2634/** A directive that provides grid-based navigation and selection behavior. */
2735@Directive ( {
@@ -37,7 +45,7 @@ import {GridPattern, GridRowPattern, GridCellPattern, GridCellWidgetPattern} fro
3745 '(pointerdown)' : '_pattern.onPointerdown($event)' ,
3846 '(pointermove)' : '_pattern.onPointermove($event)' ,
3947 '(pointerup)' : '_pattern.onPointerup($event)' ,
40- '(focusin)' : '_pattern.onFocusIn()' ,
48+ '(focusin)' : '_pattern.onFocusIn($event )' ,
4149 '(focusout)' : '_pattern.onFocusOut($event)' ,
4250 } ,
4351} )
@@ -86,6 +94,9 @@ export class Grid {
8694 /** Whether enable range selections (with modifier keys or dragging). */
8795 readonly enableRangeSelection = input ( false , { transform : booleanAttribute } ) ;
8896
97+ /** Emits when navigation occurs within the grid. */
98+ readonly onNavigate = output < NavigateEvent > ( ) ;
99+
89100 /** The UI pattern for the grid. */
90101 readonly _pattern = new GridPattern ( {
91102 ...this ,
@@ -96,26 +107,34 @@ export class Grid {
96107 constructor ( ) {
97108 afterRenderEffect ( ( ) => this . _pattern . setDefaultStateEffect ( ) ) ;
98109 afterRenderEffect ( ( ) => this . _pattern . resetStateEffect ( ) ) ;
110+ afterRenderEffect ( ( ) => this . _pattern . resetFocusEffect ( ) ) ;
111+ afterRenderEffect ( ( ) => this . _pattern . restoreFocusEffect ( ) ) ;
99112 afterRenderEffect ( ( ) => this . _pattern . focusEffect ( ) ) ;
113+ afterRenderEffect ( ( ) => {
114+ const navigateEvent = this . _pattern . lastNavigateEvent ( ) ;
115+ if ( navigateEvent ) {
116+ this . onNavigate . emit ( navigateEvent ) ;
117+ }
118+ } ) ;
100119 }
101120
102121 /** Gets the cell pattern for a given element. */
103- private _getCell ( element : Element ) : GridCellPattern | undefined {
104- const cellElement = element . closest ( '[ngGridCell]' ) ;
105- if ( cellElement === undefined ) return ;
106-
107- const widgetElement = element . closest ( '[ngGridCellWidget]' ) ;
108- for ( const row of this . _rowPatterns ( ) ) {
109- for ( const cell of row . inputs . cells ( ) ) {
110- if (
111- cell . element ( ) === cellElement ||
112- ( widgetElement !== undefined && cell . element ( ) === widgetElement )
113- ) {
114- return cell ;
122+ private _getCell ( element : Element | null | undefined ) : GridCellPattern | undefined {
123+ let target = element ;
124+
125+ while ( target ) {
126+ for ( const row of this . _rowPatterns ( ) ) {
127+ for ( const cell of row . inputs . cells ( ) ) {
128+ if ( cell . element ( ) === target ) {
129+ return cell ;
130+ }
115131 }
116132 }
133+
134+ target = target . parentElement ?. closest ( '[ngGridCell]' ) ;
117135 }
118- return ;
136+
137+ return undefined ;
119138 }
120139}
121140
@@ -125,7 +144,8 @@ export class Grid {
125144 exportAs : 'ngGridRow' ,
126145 host : {
127146 'class' : 'grid-row' ,
128- '[attr.role]' : 'role()' ,
147+ 'role' : 'row' ,
148+ '[attr.aria-rowindex]' : '_pattern.rowIndex()' ,
129149 } ,
130150} )
131151export class GridRow {
@@ -149,9 +169,6 @@ export class GridRow {
149169 /** The host native element. */
150170 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
151171
152- /** The ARIA role for the row. */
153- readonly role = input < 'row' | 'rowheader' > ( 'row' ) ;
154-
155172 /** The index of this row within the grid. */
156173 readonly rowIndex = input < number > ( ) ;
157174
@@ -180,32 +197,35 @@ export class GridRow {
180197 '[attr.aria-rowindex]' : '_pattern.ariaRowIndex()' ,
181198 '[attr.aria-colindex]' : '_pattern.ariaColIndex()' ,
182199 '[attr.aria-selected]' : '_pattern.ariaSelected()' ,
183- '[tabindex]' : '_pattern.tabIndex ()' ,
200+ '[tabindex]' : '_tabIndex ()' ,
184201 } ,
185202} )
186203export class GridCell {
187204 /** A reference to the host element. */
188205 private readonly _elementRef = inject ( ElementRef ) ;
189206
190- /** The widget contained within this cell, if any. */
191- private readonly _widgets = contentChild ( GridCellWidget ) ;
207+ /** The widgets contained within this cell, if any. */
208+ private readonly _widgets = contentChildren ( GridCellWidget , { descendants : true } ) ;
192209
193210 /** The UI pattern for the widget in this cell. */
194- private readonly _widgetPattern : Signal < GridCellWidgetPattern | undefined > = computed (
195- ( ) => this . _widgets ( ) ?. _pattern ,
211+ private readonly _widgetPatterns : Signal < GridCellWidgetPattern [ ] > = computed ( ( ) =>
212+ this . _widgets ( ) . map ( w => w . _pattern ) ,
196213 ) ;
197214
198215 /** The parent row. */
199216 private readonly _row = inject ( GridRow ) ;
200217
218+ /** Text direction. */
219+ readonly textDirection = inject ( Directionality ) . valueSignal ;
220+
201221 /** A unique identifier for the cell. */
202- private readonly _id = inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ;
222+ readonly id = input ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
203223
204224 /** The host native element. */
205225 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
206226
207227 /** The ARIA role for the cell. */
208- readonly role = input < 'gridcell' | 'columnheader' > ( 'gridcell' ) ;
228+ readonly role = input < 'gridcell' | 'columnheader' | 'rowheader' > ( 'gridcell' ) ;
209229
210230 /** The number of rows the cell should span. */
211231 readonly rowSpan = input < number > ( 1 ) ;
@@ -228,14 +248,54 @@ export class GridCell {
228248 /** Whether the cell is selectable. */
229249 readonly selectable = input < boolean > ( true ) ;
230250
251+ /** Orientation of the widgets in the cell. */
252+ readonly orientation = input < 'vertical' | 'horizontal' > ( 'horizontal' ) ;
253+
254+ /** Whether widgets navigation wraps. */
255+ readonly wrap = input ( true , { transform : booleanAttribute } ) ;
256+
257+ /** The tabindex override. */
258+ readonly tabindex = input < number | undefined > ( ) ;
259+
260+ /**
261+ * The tabindex value set to the element.
262+ * If a focus target exists then return -1. Unless an override.
263+ */
264+ protected readonly _tabIndex : Signal < number > = computed (
265+ ( ) => this . tabindex ( ) ?? this . _pattern . tabIndex ( ) ,
266+ ) ;
267+
231268 /** The UI pattern for the grid cell. */
232269 readonly _pattern = new GridCellPattern ( {
233270 ...this ,
234- id : ( ) => this . _id ,
235271 grid : this . _row . grid ,
236272 row : ( ) => this . _row . _pattern ,
237- widget : this . _widgetPattern ,
273+ widgets : this . _widgetPatterns ,
274+ getWidget : e => this . _getWidget ( e ) ,
238275 } ) ;
276+
277+ constructor ( ) { }
278+
279+ /** Gets the cell widget pattern for a given element. */
280+ private _getWidget ( element : Element | null | undefined ) : GridCellWidgetPattern | undefined {
281+ let target = element ;
282+
283+ while ( target ) {
284+ const pattern = this . _widgetPatterns ( ) . find ( w => w . element ( ) === target ) ;
285+ if ( pattern ) {
286+ return pattern ;
287+ }
288+
289+ target = target . parentElement ?. closest ( '[ngGridCellWidget]' ) ;
290+ }
291+
292+ return undefined ;
293+ }
294+
295+ /** Focuses the cell. */
296+ focus ( ) : void {
297+ this . _pattern . focus ( ) ;
298+ }
239299}
240300
241301/** A directive that represents a widget inside a grid cell. */
@@ -245,7 +305,8 @@ export class GridCell {
245305 host : {
246306 'class' : 'grid-cell-widget' ,
247307 '[attr.data-active]' : '_pattern.active()' ,
248- '[tabindex]' : '_pattern.tabIndex()' ,
308+ '[attr.data-widget-activated]' : 'isActivated()' ,
309+ '[tabindex]' : '_tabIndex()' ,
249310 } ,
250311} )
251312export class GridCellWidget {
@@ -258,17 +319,80 @@ export class GridCellWidget {
258319 /** The host native element. */
259320 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
260321
261- /** Whether the widget is activated and the grid navigation should be paused. */
262- readonly activate = model < boolean > ( false ) ;
322+ /** A unique identifier for the widget. */
323+ readonly id = input < string > ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
324+
325+ /** The type of widget, which determines how it is activated. */
326+ readonly widgetType = input < 'simple' | 'complex' | 'editable' > ( 'simple' ) ;
327+
328+ /** Whether the widget is disabled. */
329+ readonly disabled = input ( false , { transform : booleanAttribute } ) ;
330+
331+ /** The target that will receive focus instead of the widget. */
332+ readonly focusTarget = input < ElementRef | HTMLElement | undefined > ( ) ;
333+
334+ /** Emits when the widget is activated. */
335+ readonly onActivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
336+
337+ /** Emits when the widget is deactivated. */
338+ readonly onDeactivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
339+
340+ /** The tabindex override. */
341+ readonly tabindex = input < number | undefined > ( ) ;
342+
343+ /**
344+ * The tabindex value set to the element.
345+ * If a focus target exists then return -1. Unless an override.
346+ */
347+ protected readonly _tabIndex : Signal < number > = computed (
348+ ( ) => this . tabindex ( ) ?? ( this . focusTarget ( ) ? - 1 : this . _pattern . tabIndex ( ) ) ,
349+ ) ;
263350
264351 /** The UI pattern for the grid cell widget. */
265352 readonly _pattern = new GridCellWidgetPattern ( {
266353 ...this ,
267354 cell : ( ) => this . _cell . _pattern ,
355+ focusTarget : computed ( ( ) => {
356+ if ( this . focusTarget ( ) instanceof ElementRef ) {
357+ return ( this . focusTarget ( ) as ElementRef ) . nativeElement ;
358+ }
359+ return this . focusTarget ( ) ;
360+ } ) ,
268361 } ) ;
269362
363+ /** Whether the widget is activated. */
364+ get isActivated ( ) : Signal < boolean > {
365+ return this . _pattern . isActivated . asReadonly ( ) ;
366+ }
367+
368+ constructor ( ) {
369+ afterRenderEffect ( ( ) => {
370+ const activateEvent = this . _pattern . lastActivateEvent ( ) ;
371+ if ( activateEvent ) {
372+ this . onActivate . emit ( activateEvent ) ;
373+ }
374+ } ) ;
375+
376+ afterRenderEffect ( ( ) => {
377+ const deactivateEvent = this . _pattern . lastDeactivateEvent ( ) ;
378+ if ( deactivateEvent ) {
379+ this . onDeactivate . emit ( deactivateEvent ) ;
380+ }
381+ } ) ;
382+ }
383+
270384 /** Focuses the widget. */
271385 focus ( ) : void {
272- this . element ( ) . focus ( ) ;
386+ this . _pattern . focus ( ) ;
387+ }
388+
389+ /** Activates the widget. */
390+ activate ( ) : void {
391+ this . _pattern . activate ( ) ;
392+ }
393+
394+ /** Deactivates the widget. */
395+ deactivate ( ) : void {
396+ this . _pattern . deactivate ( ) ;
273397 }
274398}
0 commit comments