@@ -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/**
2735 * The container for a grid. It provides keyboard navigation and focus management for the grid's
@@ -57,7 +65,7 @@ import {GridPattern, GridRowPattern, GridCellPattern, GridCellWidgetPattern} fro
5765 '(pointerdown)' : '_pattern.onPointerdown($event)' ,
5866 '(pointermove)' : '_pattern.onPointermove($event)' ,
5967 '(pointerup)' : '_pattern.onPointerup($event)' ,
60- '(focusin)' : '_pattern.onFocusIn()' ,
68+ '(focusin)' : '_pattern.onFocusIn($event )' ,
6169 '(focusout)' : '_pattern.onFocusOut($event)' ,
6270 } ,
6371} )
@@ -127,6 +135,9 @@ export class Grid {
127135 /** Whether enable range selections (with modifier keys or dragging). */
128136 readonly enableRangeSelection = input ( false , { transform : booleanAttribute } ) ;
129137
138+ /** Emits when navigation occurs within the grid. */
139+ readonly onNavigate = output < NavigateEvent > ( ) ;
140+
130141 /** The UI pattern for the grid. */
131142 readonly _pattern = new GridPattern ( {
132143 ...this ,
@@ -137,26 +148,34 @@ export class Grid {
137148 constructor ( ) {
138149 afterRenderEffect ( ( ) => this . _pattern . setDefaultStateEffect ( ) ) ;
139150 afterRenderEffect ( ( ) => this . _pattern . resetStateEffect ( ) ) ;
151+ afterRenderEffect ( ( ) => this . _pattern . resetFocusEffect ( ) ) ;
152+ afterRenderEffect ( ( ) => this . _pattern . restoreFocusEffect ( ) ) ;
140153 afterRenderEffect ( ( ) => this . _pattern . focusEffect ( ) ) ;
154+ afterRenderEffect ( ( ) => {
155+ const navigateEvent = this . _pattern . lastNavigateEvent ( ) ;
156+ if ( navigateEvent ) {
157+ this . onNavigate . emit ( navigateEvent ) ;
158+ }
159+ } ) ;
141160 }
142161
143162 /** Gets the cell pattern for a given element. */
144- private _getCell ( element : Element ) : GridCellPattern | undefined {
145- const cellElement = element . closest ( '[ngGridCell]' ) ;
146- if ( cellElement === undefined ) return ;
147-
148- const widgetElement = element . closest ( '[ngGridCellWidget]' ) ;
149- for ( const row of this . _rowPatterns ( ) ) {
150- for ( const cell of row . inputs . cells ( ) ) {
151- if (
152- cell . element ( ) === cellElement ||
153- ( widgetElement !== undefined && cell . element ( ) === widgetElement )
154- ) {
155- return cell ;
163+ private _getCell ( element : Element | null | undefined ) : GridCellPattern | undefined {
164+ let target = element ;
165+
166+ while ( target ) {
167+ for ( const row of this . _rowPatterns ( ) ) {
168+ for ( const cell of row . inputs . cells ( ) ) {
169+ if ( cell . element ( ) === target ) {
170+ return cell ;
171+ }
156172 }
157173 }
174+
175+ target = target . parentElement ?. closest ( '[ngGridCell]' ) ;
158176 }
159- return ;
177+
178+ return undefined ;
160179 }
161180}
162181
@@ -176,7 +195,8 @@ export class Grid {
176195 exportAs : 'ngGridRow' ,
177196 host : {
178197 'class' : 'grid-row' ,
179- '[attr.role]' : 'role()' ,
198+ 'role' : 'row' ,
199+ '[attr.aria-rowindex]' : '_pattern.rowIndex()' ,
180200 } ,
181201} )
182202export class GridRow {
@@ -200,9 +220,6 @@ export class GridRow {
200220 /** The host native element. */
201221 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
202222
203- /** The ARIA role for the row. */
204- readonly role = input < 'row' | 'rowheader' > ( 'row' ) ;
205-
206223 /** The index of this row within the grid. */
207224 readonly rowIndex = input < number > ( ) ;
208225
@@ -243,32 +260,35 @@ export class GridRow {
243260 '[attr.aria-rowindex]' : '_pattern.ariaRowIndex()' ,
244261 '[attr.aria-colindex]' : '_pattern.ariaColIndex()' ,
245262 '[attr.aria-selected]' : '_pattern.ariaSelected()' ,
246- '[tabindex]' : '_pattern.tabIndex ()' ,
263+ '[tabindex]' : '_tabIndex ()' ,
247264 } ,
248265} )
249266export class GridCell {
250267 /** A reference to the host element. */
251268 private readonly _elementRef = inject ( ElementRef ) ;
252269
253- /** The widget contained within this cell, if any. */
254- private readonly _widgets = contentChild ( GridCellWidget ) ;
270+ /** The widgets contained within this cell, if any. */
271+ private readonly _widgets = contentChildren ( GridCellWidget , { descendants : true } ) ;
255272
256273 /** The UI pattern for the widget in this cell. */
257- private readonly _widgetPattern : Signal < GridCellWidgetPattern | undefined > = computed (
258- ( ) => this . _widgets ( ) ?. _pattern ,
274+ private readonly _widgetPatterns : Signal < GridCellWidgetPattern [ ] > = computed ( ( ) =>
275+ this . _widgets ( ) . map ( w => w . _pattern ) ,
259276 ) ;
260277
261278 /** The parent row. */
262279 private readonly _row = inject ( GridRow ) ;
263280
281+ /** Text direction. */
282+ readonly textDirection = inject ( Directionality ) . valueSignal ;
283+
264284 /** A unique identifier for the cell. */
265- private readonly _id = inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ;
285+ readonly id = input ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
266286
267287 /** The host native element. */
268288 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
269289
270290 /** The ARIA role for the cell. */
271- readonly role = input < 'gridcell' | 'columnheader' > ( 'gridcell' ) ;
291+ readonly role = input < 'gridcell' | 'columnheader' | 'rowheader' > ( 'gridcell' ) ;
272292
273293 /** The number of rows the cell should span. */
274294 readonly rowSpan = input < number > ( 1 ) ;
@@ -291,14 +311,49 @@ export class GridCell {
291311 /** Whether the cell is selectable. */
292312 readonly selectable = input < boolean > ( true ) ;
293313
314+ /** Orientation of the widgets in the cell. */
315+ readonly orientation = input < 'vertical' | 'horizontal' > ( 'horizontal' ) ;
316+
317+ /** Whether widgets navigation wraps. */
318+ readonly wrap = input ( true , { transform : booleanAttribute } ) ;
319+
320+ /** The tabindex override. */
321+ readonly tabindex = input < number | undefined > ( ) ;
322+
323+ /**
324+ * The tabindex value set to the element.
325+ * If a focus target exists then return -1. Unless an override.
326+ */
327+ protected readonly _tabIndex : Signal < number > = computed (
328+ ( ) => this . tabindex ( ) ?? this . _pattern . tabIndex ( ) ,
329+ ) ;
330+
294331 /** The UI pattern for the grid cell. */
295332 readonly _pattern = new GridCellPattern ( {
296333 ...this ,
297- id : ( ) => this . _id ,
298334 grid : this . _row . grid ,
299335 row : ( ) => this . _row . _pattern ,
300- widget : this . _widgetPattern ,
336+ widgets : this . _widgetPatterns ,
337+ getWidget : e => this . _getWidget ( e ) ,
301338 } ) ;
339+
340+ constructor ( ) { }
341+
342+ /** Gets the cell widget pattern for a given element. */
343+ private _getWidget ( element : Element | null | undefined ) : GridCellWidgetPattern | undefined {
344+ let target = element ;
345+
346+ while ( target ) {
347+ const pattern = this . _widgetPatterns ( ) . find ( w => w . element ( ) === target ) ;
348+ if ( pattern ) {
349+ return pattern ;
350+ }
351+
352+ target = target . parentElement ?. closest ( '[ngGridCellWidget]' ) ;
353+ }
354+
355+ return undefined ;
356+ }
302357}
303358
304359/**
@@ -323,7 +378,8 @@ export class GridCell {
323378 host : {
324379 'class' : 'grid-cell-widget' ,
325380 '[attr.data-active]' : '_pattern.active()' ,
326- '[tabindex]' : '_pattern.tabIndex()' ,
381+ '[attr.data-widget-activated]' : 'isActivated()' ,
382+ '[tabindex]' : '_tabIndex()' ,
327383 } ,
328384} )
329385export class GridCellWidget {
@@ -336,17 +392,75 @@ export class GridCellWidget {
336392 /** The host native element. */
337393 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
338394
339- /** Whether the widget is activated and the grid navigation should be paused. */
340- readonly activate = model < boolean > ( false ) ;
395+ /** A unique identifier for the widget. */
396+ readonly id = input < string > ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
397+
398+ /** The type of widget, which determines how it is activated. */
399+ readonly widgetType = input < 'simple' | 'complex' | 'editable' > ( 'simple' ) ;
400+
401+ /** Whether the widget is disabled. */
402+ readonly disabled = input ( false , { transform : booleanAttribute } ) ;
403+
404+ /** The target that will receive focus instead of the widget. */
405+ readonly focusTarget = input < ElementRef | HTMLElement | undefined > ( ) ;
406+
407+ /** Emits when the widget is activated. */
408+ readonly onActivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
409+
410+ /** Emits when the widget is deactivated. */
411+ readonly onDeactivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
412+
413+ /** The tabindex override. */
414+ readonly tabindex = input < number | undefined > ( ) ;
415+
416+ /**
417+ * The tabindex value set to the element.
418+ * If a focus target exists then return -1. Unless an override.
419+ */
420+ protected readonly _tabIndex : Signal < number > = computed (
421+ ( ) => this . tabindex ( ) ?? ( this . focusTarget ( ) ? - 1 : this . _pattern . tabIndex ( ) ) ,
422+ ) ;
341423
342424 /** The UI pattern for the grid cell widget. */
343425 readonly _pattern = new GridCellWidgetPattern ( {
344426 ...this ,
345427 cell : ( ) => this . _cell . _pattern ,
428+ focusTarget : computed ( ( ) => {
429+ if ( this . focusTarget ( ) instanceof ElementRef ) {
430+ return ( this . focusTarget ( ) as ElementRef ) . nativeElement ;
431+ }
432+ return this . focusTarget ( ) ;
433+ } ) ,
346434 } ) ;
347435
348- /** Focuses the widget. */
349- focus ( ) : void {
350- this . element ( ) . focus ( ) ;
436+ /** Whether the widget is activated. */
437+ get isActivated ( ) : Signal < boolean > {
438+ return this . _pattern . isActivated . asReadonly ( ) ;
439+ }
440+
441+ constructor ( ) {
442+ afterRenderEffect ( ( ) => {
443+ const activateEvent = this . _pattern . lastActivateEvent ( ) ;
444+ if ( activateEvent ) {
445+ this . onActivate . emit ( activateEvent ) ;
446+ }
447+ } ) ;
448+
449+ afterRenderEffect ( ( ) => {
450+ const deactivateEvent = this . _pattern . lastDeactivateEvent ( ) ;
451+ if ( deactivateEvent ) {
452+ this . onDeactivate . emit ( deactivateEvent ) ;
453+ }
454+ } ) ;
455+ }
456+
457+ /** Activates the widget. */
458+ activate ( ) : void {
459+ this . _pattern . activate ( ) ;
460+ }
461+
462+ /** Deactivates the widget. */
463+ deactivate ( ) : void {
464+ this . _pattern . deactivate ( ) ;
351465 }
352466}
0 commit comments