Skip to content

Commit 86ec33f

Browse files
committed
feat(docs): enhance README and demo with toolbar position behavior details
1 parent 5045ca7 commit 86ec33f

4 files changed

Lines changed: 116 additions & 48 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ Add the `<row-actions>` component inside a `mat-cell`:
106106

107107
### Position Behavior
108108

109-
The toolbar automatically detects its position within the cell:
110-
- **First child** in cell: Toolbar appears from the **left**
111-
- **Last child** in cell: Toolbar appears from the **right**
109+
The toolbar automatically detects its position within the cell and animates accordingly:
110+
- **First child** in cell → Toolbar appears from the **left**
111+
- **Last child** in cell → Toolbar appears from the **right**
112+
113+
You can place `<row-actions>` in **any cell** of your table, not just a dedicated "actions" column. This allows you to add contextual actions to specific data columns.
112114

113115
## Examples
114116

projects/demo/src/app/app.component.html

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ <h2>Demo</h2>
6868
</div>
6969

7070
<!-- Demo Table -->
71-
<p class="demo-hint">Hover over a row to see the actions appear</p>
71+
<p class="demo-hint">Hover over a row to see the actions appear. Notice how the toolbar appears from the <strong>left</strong> on the Name column and from the <strong>right</strong> on Email and Actions columns, depending on the position of <code>&lt;row-actions&gt;</code> within the cell.</p>
7272

7373
<div class="demo-table-wrapper">
7474
<mat-table [dataSource]="dataSource" class="demo-table">
@@ -79,12 +79,26 @@ <h2>Demo</h2>
7979

8080
<ng-container matColumnDef="name">
8181
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
82-
<mat-cell *matCellDef="let user">{{ user.name }}</mat-cell>
82+
<mat-cell *matCellDef="let user">
83+
<row-actions>
84+
<button mat-icon-button (click)="onEdit(user)" title="Edit">
85+
<mat-icon>edit</mat-icon>
86+
</button>
87+
</row-actions>
88+
{{ user.name }}
89+
</mat-cell>
8390
</ng-container>
8491

8592
<ng-container matColumnDef="email">
8693
<mat-header-cell *matHeaderCellDef>Email</mat-header-cell>
87-
<mat-cell *matCellDef="let user">{{ user.email }}</mat-cell>
94+
<mat-cell *matCellDef="let user">
95+
{{ user.email }}
96+
<row-actions>
97+
<button mat-icon-button (click)="onEdit(user)" title="Edit">
98+
<mat-icon>edit</mat-icon>
99+
</button>
100+
</row-actions>
101+
</mat-cell>
88102
</ng-container>
89103

90104
<ng-container matColumnDef="role">
@@ -189,6 +203,28 @@ <h2>API</h2>
189203
</div>
190204
</section>
191205

206+
<!-- Position Behavior Section -->
207+
<section class="section">
208+
<h2>Position Behavior</h2>
209+
<p>The toolbar automatically detects its position within the cell and animates accordingly:</p>
210+
<ul class="feature-list">
211+
<li><strong>First child</strong> in cell → Toolbar appears from the <strong>left</strong></li>
212+
<li><strong>Last child</strong> in cell → Toolbar appears from the <strong>right</strong></li>
213+
</ul>
214+
<p>You can place <code>&lt;row-actions&gt;</code> in any cell of your table, not just a dedicated "actions" column. This allows you to add contextual actions to specific data columns.</p>
215+
<pre ngNonBindable><code class="language-html">&lt;!-- Toolbar from LEFT --&gt;
216+
&lt;mat-cell *matCellDef="let element"&gt;
217+
&lt;row-actions&gt;...&lt;/row-actions&gt;
218+
{{ element.name }}
219+
&lt;/mat-cell&gt;
220+
221+
&lt;!-- Toolbar from RIGHT --&gt;
222+
&lt;mat-cell *matCellDef="let element"&gt;
223+
{{ element.name }}
224+
&lt;row-actions&gt;...&lt;/row-actions&gt;
225+
&lt;/mat-cell&gt;</code></pre>
226+
</section>
227+
192228
<!-- Card Footer -->
193229
<footer class="card-footer">
194230
<img src="https://www.softwarity.io/img/softwarity.svg" alt="Softwarity" class="footer-logo">

projects/demo/src/app/app.component.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@
129129
color: var(--text-secondary);
130130
margin: 0 0 12px 0;
131131
}
132+
133+
ul.feature-list {
134+
color: var(--text-secondary);
135+
margin: 0 0 16px 0;
136+
padding-left: 24px;
137+
138+
li {
139+
margin-bottom: 8px;
140+
line-height: 1.5;
141+
}
142+
}
132143
}
133144

134145
// API Table

src/lib/row-actions.component.ts

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import { animate, state, style, transition, trigger } from '@angular/animations';
2-
import { AsyncPipe, NgStyle } from '@angular/common';
3-
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input } from '@angular/core';
4-
import {MatToolbarModule} from '@angular/material/toolbar';
5-
import {ConnectedPosition, OverlayModule} from '@angular/cdk/overlay';
6-
import { Subject } from 'rxjs';
2+
import { AsyncPipe } from '@angular/common';
3+
import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, HostBinding, inject, input } from '@angular/core';
4+
import { MatToolbarModule } from '@angular/material/toolbar';
5+
import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay';
6+
import { BehaviorSubject } from 'rxjs';
77
import { ThemePalette } from '@angular/material/core';
88

99
@Component({
1010
selector: 'row-actions',
1111
standalone: true,
1212
template: `
13-
@if (!disabled) {
13+
@if (!disabled()) {
1414
<span class="actions-trigger" cdkOverlayOrigin #trigger="cdkOverlayOrigin"></span>
1515
<ng-template cdkConnectedOverlay [cdkConnectedOverlayPositions]="overlayPositions" [cdkConnectedOverlayOrigin]="trigger" [cdkConnectedOverlayOpen]="!!(open$ | async)">
16-
<mat-toolbar [ngStyle]="{height: heightToolbar, minHeight: heightToolbar, maxHeight: heightToolbar}" [color]="color" [@expandFromRight]="animatedFrom" [@expandFromLeft]="animatedFrom">
16+
<mat-toolbar [style.height]="heightToolbar" [style.min-height]="heightToolbar" [style.max-height]="heightToolbar" [color]="color()" [@expandFromRight]="animatedFrom" [@expandFromLeft]="animatedFrom">
1717
<ng-content></ng-content>
1818
</mat-toolbar>
1919
</ng-template>
@@ -47,7 +47,6 @@ import { ThemePalette } from '@angular/material/core';
4747
`],
4848
imports: [
4949
AsyncPipe,
50-
NgStyle,
5150
MatToolbarModule,
5251
OverlayModule,
5352
],
@@ -69,76 +68,96 @@ import { ThemePalette } from '@angular/material/core';
6968
})
7069
export class RowActionComponent implements AfterViewInit {
7170

72-
matRowElement: any;
71+
private readonly el = inject(ElementRef<HTMLElement>);
72+
private readonly destroyRef = inject(DestroyRef);
73+
74+
private matRowElement: HTMLElement | null = null;
75+
private mouseEnterListener: (() => void) | null = null;
7376

7477
overlayPositions: ConnectedPosition[] = [{ originY: 'top', originX: 'end', overlayY: 'top', overlayX: 'end' }];
7578

76-
open$: Subject<boolean> = new Subject<boolean>();
79+
open$ = new BehaviorSubject<boolean>(false);
7780

78-
heightToolbar: string = '48px';
81+
heightToolbar = '48px';
7982

8083
position: 'left' | 'right' = 'right';
8184

82-
@Input()
83-
animationDisabled: boolean = false;
85+
readonly animationDisabled = input<boolean>(false);
8486

8587
animatedFrom: 'left' | 'right' | null = null;
8688

87-
constructor(private el: ElementRef) {}
89+
readonly color = input<ThemePalette>('primary');
90+
91+
readonly disabled = input<boolean | null>(false);
92+
93+
// Host bindings for positioning
94+
@HostBinding('style.margin-right.px')
95+
marginRight = 0;
96+
97+
@HostBinding('style.flex-grow')
98+
flexGrow = 0;
99+
100+
@HostBinding('style.left.px')
101+
left = 0;
88102

89103
ngAfterViewInit(): void {
90104
const parentElement = this.el.nativeElement.parentElement;
105+
if (!parentElement) {
106+
return;
107+
}
108+
91109
const parentStyle = getComputedStyle(parentElement);
92110
this.position = parentElement.childNodes[0] === this.el.nativeElement ? 'left' : 'right';
93111
this.animatedFrom = this.position;
112+
94113
if (this.position === 'left') {
95114
this.overlayPositions = [{ originY: 'top', originX: 'start', overlayY: 'top', overlayX: 'start' }];
96115
this.flexGrow = 0;
97116
this.left = -parseFloat(parentStyle.paddingLeft);
98-
} else { // We're right
117+
} else {
99118
this.overlayPositions = [{ originY: 'top', originX: 'end', overlayY: 'top', overlayX: 'end' }];
100119
this.flexGrow = 1;
101-
this.marginRight = -parseFloat(parentStyle.paddingRight);;
120+
this.marginRight = -parseFloat(parentStyle.paddingRight);
102121
}
103-
if (this.animationDisabled) {
122+
123+
if (this.animationDisabled()) {
104124
this.animatedFrom = null;
105125
}
106-
this.matRowElement = this.el.nativeElement.closest('tr[mat-row], mat-row');
107-
this.matRowElement.addEventListener('mouseenter', () => {
108-
const parentStyle = getComputedStyle(parentElement);
109-
this.heightToolbar = parentStyle.height;
126+
127+
this.matRowElement = this.el.nativeElement.closest('tr[mat-row], mat-row') as HTMLElement | null;
128+
if (!this.matRowElement) {
129+
return;
130+
}
131+
132+
this.mouseEnterListener = () => {
133+
const currentParentStyle = getComputedStyle(parentElement);
134+
this.heightToolbar = currentParentStyle.height;
110135
this.open$.next(true);
111136
document.addEventListener('mousemove', this.mouseMoveListener);
137+
};
138+
139+
this.matRowElement.addEventListener('mouseenter', this.mouseEnterListener);
140+
141+
// Cleanup on destroy
142+
this.destroyRef.onDestroy(() => {
143+
this.open$.complete();
144+
document.removeEventListener('mousemove', this.mouseMoveListener);
145+
if (this.matRowElement && this.mouseEnterListener) {
146+
this.matRowElement.removeEventListener('mouseenter', this.mouseEnterListener);
147+
}
112148
});
113149
}
114150

115-
mouseMoveListener: EventListenerOrEventListenerObject = ($event: any) => {
151+
private readonly mouseMoveListener = (event: MouseEvent): void => {
116152
if (this.matRowElement) {
117153
const rect = this.matRowElement.getBoundingClientRect();
118-
const isInHorizontalBounds = $event.clientX >= rect.left && $event.clientX <= rect.right;
119-
const isInVerticalBounds = $event.clientY >= rect.top && $event.clientY <= rect.bottom;
154+
const isInHorizontalBounds = event.clientX >= rect.left && event.clientX <= rect.right;
155+
const isInVerticalBounds = event.clientY >= rect.top && event.clientY <= rect.bottom;
120156
if (isInHorizontalBounds && isInVerticalBounds) {
121157
return;
122158
}
123159
}
124160
this.open$.next(false);
125161
document.removeEventListener('mousemove', this.mouseMoveListener);
126-
}
127-
128-
// We're right
129-
@HostBinding('style.margin-right.px')
130-
marginRight: number = 0;
131-
@HostBinding('style.flex-grow')
132-
flexGrow: number = 0;
133-
134-
// We're left
135-
@HostBinding('style.left.px')
136-
left: number = 0;
137-
138-
139-
@Input()
140-
color: ThemePalette = 'primary';
141-
142-
@Input()
143-
disabled: boolean | null = false;
162+
};
144163
}

0 commit comments

Comments
 (0)