@@ -11,12 +11,12 @@ 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' ;
@@ -57,7 +57,7 @@ import {GridPattern, GridRowPattern, GridCellPattern, GridCellWidgetPattern} fro
5757 '(pointerdown)' : '_pattern.onPointerdown($event)' ,
5858 '(pointermove)' : '_pattern.onPointermove($event)' ,
5959 '(pointerup)' : '_pattern.onPointerup($event)' ,
60- '(focusin)' : '_pattern.onFocusIn()' ,
60+ '(focusin)' : '_pattern.onFocusIn($event )' ,
6161 '(focusout)' : '_pattern.onFocusOut($event)' ,
6262 } ,
6363} )
@@ -137,26 +137,28 @@ export class Grid {
137137 constructor ( ) {
138138 afterRenderEffect ( ( ) => this . _pattern . setDefaultStateEffect ( ) ) ;
139139 afterRenderEffect ( ( ) => this . _pattern . resetStateEffect ( ) ) ;
140+ afterRenderEffect ( ( ) => this . _pattern . resetFocusEffect ( ) ) ;
141+ afterRenderEffect ( ( ) => this . _pattern . restoreFocusEffect ( ) ) ;
140142 afterRenderEffect ( ( ) => this . _pattern . focusEffect ( ) ) ;
141143 }
142144
143145 /** 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 ;
146+ private _getCell ( element : Element | null | undefined ) : GridCellPattern | undefined {
147+ let target = element ;
148+
149+ while ( target ) {
150+ for ( const row of this . _rowPatterns ( ) ) {
151+ for ( const cell of row . inputs . cells ( ) ) {
152+ if ( cell . element ( ) === target ) {
153+ return cell ;
154+ }
156155 }
157156 }
157+
158+ target = target . parentElement ?. closest ( '[ngGridCell]' ) ;
158159 }
159- return ;
160+
161+ return undefined ;
160162 }
161163}
162164
@@ -176,7 +178,8 @@ export class Grid {
176178 exportAs : 'ngGridRow' ,
177179 host : {
178180 'class' : 'grid-row' ,
179- '[attr.role]' : 'role()' ,
181+ 'role' : 'row' ,
182+ '[attr.aria-rowindex]' : '_pattern.rowIndex()' ,
180183 } ,
181184} )
182185export class GridRow {
@@ -200,9 +203,6 @@ export class GridRow {
200203 /** The host native element. */
201204 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
202205
203- /** The ARIA role for the row. */
204- readonly role = input < 'row' | 'rowheader' > ( 'row' ) ;
205-
206206 /** The index of this row within the grid. */
207207 readonly rowIndex = input < number > ( ) ;
208208
@@ -243,32 +243,35 @@ export class GridRow {
243243 '[attr.aria-rowindex]' : '_pattern.ariaRowIndex()' ,
244244 '[attr.aria-colindex]' : '_pattern.ariaColIndex()' ,
245245 '[attr.aria-selected]' : '_pattern.ariaSelected()' ,
246- '[tabindex]' : '_pattern.tabIndex ()' ,
246+ '[tabindex]' : '_tabIndex ()' ,
247247 } ,
248248} )
249249export class GridCell {
250250 /** A reference to the host element. */
251251 private readonly _elementRef = inject ( ElementRef ) ;
252252
253- /** The widget contained within this cell, if any. */
254- private readonly _widgets = contentChild ( GridCellWidget ) ;
253+ /** The widgets contained within this cell, if any. */
254+ private readonly _widgets = contentChildren ( GridCellWidget , { descendants : true } ) ;
255255
256256 /** The UI pattern for the widget in this cell. */
257- private readonly _widgetPattern : Signal < GridCellWidgetPattern | undefined > = computed (
258- ( ) => this . _widgets ( ) ?. _pattern ,
257+ private readonly _widgetPatterns : Signal < GridCellWidgetPattern [ ] > = computed ( ( ) =>
258+ this . _widgets ( ) . map ( w => w . _pattern ) ,
259259 ) ;
260260
261261 /** The parent row. */
262262 private readonly _row = inject ( GridRow ) ;
263263
264+ /** Text direction. */
265+ readonly textDirection = inject ( Directionality ) . valueSignal ;
266+
264267 /** A unique identifier for the cell. */
265- private readonly _id = inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ;
268+ readonly id = input ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
266269
267270 /** The host native element. */
268271 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
269272
270273 /** The ARIA role for the cell. */
271- readonly role = input < 'gridcell' | 'columnheader' > ( 'gridcell' ) ;
274+ readonly role = input < 'gridcell' | 'columnheader' | 'rowheader' > ( 'gridcell' ) ;
272275
273276 /** The number of rows the cell should span. */
274277 readonly rowSpan = input < number > ( 1 ) ;
@@ -291,14 +294,49 @@ export class GridCell {
291294 /** Whether the cell is selectable. */
292295 readonly selectable = input < boolean > ( true ) ;
293296
297+ /** Orientation of the widgets in the cell. */
298+ readonly orientation = input < 'vertical' | 'horizontal' > ( 'horizontal' ) ;
299+
300+ /** Whether widgets navigation wraps. */
301+ readonly wrap = input ( true , { transform : booleanAttribute } ) ;
302+
303+ /** The tabindex override. */
304+ readonly tabindex = input < number | undefined > ( ) ;
305+
306+ /**
307+ * The tabindex value set to the element.
308+ * If a focus target exists then return -1. Unless an override.
309+ */
310+ protected readonly _tabIndex : Signal < number > = computed (
311+ ( ) => this . tabindex ( ) ?? this . _pattern . tabIndex ( ) ,
312+ ) ;
313+
294314 /** The UI pattern for the grid cell. */
295315 readonly _pattern = new GridCellPattern ( {
296316 ...this ,
297- id : ( ) => this . _id ,
298317 grid : this . _row . grid ,
299318 row : ( ) => this . _row . _pattern ,
300- widget : this . _widgetPattern ,
319+ widgets : this . _widgetPatterns ,
320+ getWidget : e => this . _getWidget ( e ) ,
301321 } ) ;
322+
323+ constructor ( ) { }
324+
325+ /** Gets the cell widget pattern for a given element. */
326+ private _getWidget ( element : Element | null | undefined ) : GridCellWidgetPattern | undefined {
327+ let target = element ;
328+
329+ while ( target ) {
330+ const pattern = this . _widgetPatterns ( ) . find ( w => w . element ( ) === target ) ;
331+ if ( pattern ) {
332+ return pattern ;
333+ }
334+
335+ target = target . parentElement ?. closest ( '[ngGridCellWidget]' ) ;
336+ }
337+
338+ return undefined ;
339+ }
302340}
303341
304342/**
@@ -323,7 +361,8 @@ export class GridCell {
323361 host : {
324362 'class' : 'grid-cell-widget' ,
325363 '[attr.data-active]' : '_pattern.active()' ,
326- '[tabindex]' : '_pattern.tabIndex()' ,
364+ '[attr.data-active-control]' : 'isActivated() ? "widget" : "cell"' ,
365+ '[tabindex]' : '_tabIndex()' ,
327366 } ,
328367} )
329368export class GridCellWidget {
@@ -336,17 +375,75 @@ export class GridCellWidget {
336375 /** The host native element. */
337376 readonly element = computed ( ( ) => this . _elementRef . nativeElement ) ;
338377
339- /** Whether the widget is activated and the grid navigation should be paused. */
340- readonly activate = model < boolean > ( false ) ;
378+ /** A unique identifier for the widget. */
379+ readonly id = input < string > ( inject ( _IdGenerator ) . getId ( 'ng-grid-cell-' , true ) ) ;
380+
381+ /** The type of widget, which determines how it is activated. */
382+ readonly widgetType = input < 'simple' | 'complex' | 'editable' > ( 'simple' ) ;
383+
384+ /** Whether the widget is disabled. */
385+ readonly disabled = input ( false , { transform : booleanAttribute } ) ;
386+
387+ /** The target that will receive focus instead of the widget. */
388+ readonly focusTarget = input < ElementRef | HTMLElement | undefined > ( ) ;
389+
390+ /** Emits when the widget is activated. */
391+ readonly onActivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
392+
393+ /** Emits when the widget is deactivated. */
394+ readonly onDeactivate = output < KeyboardEvent | FocusEvent | undefined > ( ) ;
395+
396+ /** The tabindex override. */
397+ readonly tabindex = input < number | undefined > ( ) ;
398+
399+ /**
400+ * The tabindex value set to the element.
401+ * If a focus target exists then return -1. Unless an override.
402+ */
403+ protected readonly _tabIndex : Signal < number > = computed (
404+ ( ) => this . tabindex ( ) ?? ( this . focusTarget ( ) ? - 1 : this . _pattern . tabIndex ( ) ) ,
405+ ) ;
341406
342407 /** The UI pattern for the grid cell widget. */
343408 readonly _pattern = new GridCellWidgetPattern ( {
344409 ...this ,
345410 cell : ( ) => this . _cell . _pattern ,
411+ focusTarget : computed ( ( ) => {
412+ if ( this . focusTarget ( ) instanceof ElementRef ) {
413+ return ( this . focusTarget ( ) as ElementRef ) . nativeElement ;
414+ }
415+ return this . focusTarget ( ) ;
416+ } ) ,
346417 } ) ;
347418
348- /** Focuses the widget. */
349- focus ( ) : void {
350- this . element ( ) . focus ( ) ;
419+ /** Whether the widget is activated. */
420+ get isActivated ( ) : Signal < boolean > {
421+ return this . _pattern . isActivated . asReadonly ( ) ;
422+ }
423+
424+ constructor ( ) {
425+ afterRenderEffect ( ( ) => {
426+ const activateEvent = this . _pattern . lastActivateEvent ( ) ;
427+ if ( activateEvent ) {
428+ this . onActivate . emit ( activateEvent ) ;
429+ }
430+ } ) ;
431+
432+ afterRenderEffect ( ( ) => {
433+ const deactivateEvent = this . _pattern . lastDeactivateEvent ( ) ;
434+ if ( deactivateEvent ) {
435+ this . onDeactivate . emit ( deactivateEvent ) ;
436+ }
437+ } ) ;
438+ }
439+
440+ /** Activates the widget. */
441+ activate ( ) : void {
442+ this . _pattern . activate ( ) ;
443+ }
444+
445+ /** Deactivates the widget. */
446+ deactivate ( ) : void {
447+ this . _pattern . deactivate ( ) ;
351448 }
352449}
0 commit comments