Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/app/compose/mailrecipientinput.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<mat-form-field style="width: 100%" floatPlaceholder="auto">
<mat-chip-list #chipList>
<mat-form-field class="recipient-field" floatPlaceholder="auto">
<mat-chip-list #chipList class="recipient-chip-list">
<mat-chip *ngFor="let recipient of recipientsList; let ndx=index" [selectable]="selectable"
style="background-color: #e3f2fd;"
class="recipient-chip"
[removable]="true"
(removed)="removeRecipient(ndx)">
{{recipient.nameAndAddress}}
<span class="recipient-chip-label">{{recipient.nameAndAddress}}</span>
<mat-icon matChipRemove svgIcon="close"></mat-icon>
</mat-chip>
<input #searchTextInput [formControl]="searchTextFormControl" [placeholder]="placeholder"
Expand All @@ -13,16 +13,16 @@
[matChipInputAddOnBlur]="false"
(blur)="addRecipientFromBlur()"
(matChipInputTokenEnd)="addRecipientFromEnter($event)"
[matAutocomplete]="auto" style="flex-grow: 1"
[matAutocomplete]="auto" class="recipient-search-input"
type="email"
/>
<mat-error *ngIf="invalidemail">
Please enter a valid email address
</mat-error>
</mat-chip-list>
</mat-form-field>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="addRecipientFromAutoComplete($event.option.value)">
<mat-option *ngFor="let recipient of filteredRecipients | async" [value]="recipient">
{{ recipient.name }}
<mat-autocomplete #auto="matAutocomplete" [panelWidth]="autocompletePanelWidth" (optionSelected)="addRecipientFromAutoComplete($event.option.value)">
<mat-option *ngFor="let recipient of filteredRecipients | async" [value]="recipient" class="recipient-option">
<span class="recipient-option-label">{{ recipient.name }}</span>
</mat-option>
</mat-autocomplete>
51 changes: 51 additions & 0 deletions src/app/compose/mailrecipientinput.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
:host {
display: block;
width: 100%;
min-width: 0;
}

.recipient-field {
width: 100%;
}

.recipient-chip-list {
width: 100%;
}

.recipient-chip {
background-color: #e3f2fd;
height: auto;
min-height: 32px;
max-width: 100%;
white-space: normal;
}

.recipient-chip-label,
.recipient-option-label {
line-height: 18px;
overflow-wrap: anywhere;
white-space: normal;
}

.recipient-search-input {
flex: 1 1 180px;
min-width: 180px;
}

.recipient-option {
height: auto;
min-height: 48px;
padding-bottom: 8px;
padding-top: 8px;
}

@media only screen and (max-width: 600px) {
.recipient-chip {
width: 100%;
}

.recipient-search-input {
flex-basis: 100%;
min-width: 0;
}
}
101 changes: 101 additions & 0 deletions src/app/compose/mailrecipientinput.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// --------- BEGIN RUNBOX LICENSE ---------
// Copyright (C) 2016-2026 Runbox Solutions AS (runbox.com).
//
// This file is part of Runbox 7.
//
// Runbox 7 is free software: You can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// Runbox 7 is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Runbox 7. If not, see <https://www.gnu.org/licenses/>.
// ---------- END RUNBOX LICENSE ----------

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatLegacyAutocomplete as MatAutocomplete, MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar';
import { BehaviorSubject, of } from 'rxjs';

import { MailAddressInfo } from '../common/mailaddressinfo';
import { MailRecipientInputComponent } from './mailrecipientinput.component';
import { Recipient } from './recipient';
import { RecipientsService } from './recipients.service';

class MockRecipientsService {
recipients = new BehaviorSubject<Recipient[]>([
new Recipient(['"Alice Example" <alice.long.address@example.com>'])
]);
}

class MockSnackBar {
open() {
return {
onAction: () => of(undefined)
};
}
}

describe('MailRecipientInputComponent', () => {
let fixture: ComponentFixture<MailRecipientInputComponent>;
let component: MailRecipientInputComponent;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
MatAutocompleteModule,
MatChipsModule,
MatFormFieldModule,
MatIconTestingModule,
MatInputModule,
NoopAnimationsModule,
ReactiveFormsModule
],
declarations: [
MailRecipientInputComponent
],
providers: [
{ provide: RecipientsService, useClass: MockRecipientsService },
{ provide: MatSnackBar, useClass: MockSnackBar }
]
});

fixture = TestBed.createComponent(MailRecipientInputComponent);
component = fixture.componentInstance;
component.placeholder = 'To';
component.recipients = [
MailAddressInfo.parse('"A very long display name" <very.long.address@example.com>')[0]
];
component.ngOnChanges();
fixture.detectChanges();
});

it('uses a viewport-aware autocomplete panel width for mobile selection', () => {
const autocomplete = fixture.debugElement.query(By.directive(MatAutocomplete)).componentInstance as MatAutocomplete;

expect((component as any).autocompletePanelWidth).toBe('min(95vw, 520px)');
expect(autocomplete.panelWidth).toBe('min(95vw, 520px)');
});

it('marks long recipient chips and the search input with responsive classes', () => {
const chip = fixture.debugElement.query(By.css('mat-chip'));
const chipLabel = fixture.debugElement.query(By.css('.recipient-chip-label'));
const input = fixture.debugElement.query(By.css('input'));

expect(chip.nativeElement.classList).toContain('recipient-chip');
expect(chipLabel.nativeElement.textContent).toContain('very.long.address@example.com');
expect(input.nativeElement.classList).toContain('recipient-search-input');
});
});
4 changes: 3 additions & 1 deletion src/app/compose/mailrecipientinput.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ const COMMA = 188;
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'mailrecipient-input',
templateUrl: 'mailrecipientinput.component.html'
templateUrl: 'mailrecipientinput.component.html',
styleUrls: ['mailrecipientinput.component.scss']
})
export class MailRecipientInputComponent implements OnChanges, AfterViewInit {
autocompletePanelWidth = 'min(95vw, 520px)';
filteredRecipients: BehaviorSubject<Recipient[]> = new BehaviorSubject([]);

searchTextFormControl: UntypedFormControl = new UntypedFormControl();
Expand Down