Sharing Settings UI/UX Improvements
Overview
Major UX improvements have been implemented for the sharing settings dialog to make the invitation process clearer and more intuitive. The new design addresses user confusion about when invitations are actually sent.
Key Improvements
✅ 1. Role Selector Dropdown
- Location: Right side of email input field
- Options: Reader, Prompter, Editor (Owner not available for invitation)
- Default: Reader role
- Tooltips: Hover over roles to see helpful descriptions
✅ 2. Email Chip Input System
- Enter Key: Converts typed email into a visual chip (with validation)
- True Material Design: Both states use real
mat-form-fieldfor perfect consistency - Height Consistency: Both text and chip states maintain exact same height (56px)
- Immediate Focus: Chip form field becomes active after creation for instant keyboard interaction
- Backspace/Delete: Converts chip back to editable text with cursor positioning
- Email Validation: Comprehensive validation with prominent Material Design error styling (red borders, labels, and text)
- Perfect Styling: Identical focus states, label animations, and error handling
- Single Email: Only one invitation at a time (for now)
✅ 3. Smart Button Behavior
- Dynamic Buttons: "Done" transforms to "Share" when invitation ready
- Clear Action: "Share" button makes it obvious when invitation happens
- Cancel Option: Always available to close dialog without changes
✅ 4. Email Validation System
- Real-time Validation: Checks email format and uniqueness
- Material Design Errors: Native Angular Material error handling
- Prevention Logic: Blocks invalid emails from becoming chips
- Smart Clearing: Errors clear when user starts typing
- Proper Form Structure: Chips and inputs use separate containers for Angular Material compatibility
✅ 5. Improved Visual Layout
- Side-by-Side: Email and role selector in horizontal layout
- Consistent Spacing: Better visual hierarchy
- Action Separation: Copy link on left, main actions on right
New User Flow
Step 1: Enter Email
[Email Input Field] [Role Dropdown ▼]
User types: "user@example.com"
Step 2: Press Enter
[user@example.com ×] [Reader ▼]
Email becomes a chip with remove button
Step 3: Choose Role (Optional)
[user@example.com ×] [Editor ▼]
User can change from Reader to Editor/Prompter
Step 4: Send Invitation
[Cancel] [Share]
User clicks "Share" - invitation actually sent
Technical Implementation
Component Updates - ShareDialogComponent
New Properties:
selectedRole: number = 1; // Default to Reader
emailChip: string | null = null; // Email in chip form
availableRoles = [ // Role options for dropdown
{ value: 1, name: 'Reader', description: 'Can view project content' },
{ value: 2, name: 'Prompter', description: 'Can use AI prompts' },
{ value: 3, name: 'Editor', description: 'Can edit project content' }
];
New Methods:
// Handle Enter/Backspace in email input
onEmailKeyDown(event: KeyboardEvent): void
// Handle Backspace/Delete on chip form field (with cursor positioning)
onChipKeyDown(event: KeyboardEvent): void
// Convert email text to chip with auto-focus
createEmailChip(): void
// Convert chip back to text with auto-focus (keyboard + click)
removeEmailChip(): void
// Email validation
isValidEmail(email: string): boolean
// Email validation
validateEmail(email: string): string | null
// Check if invitation is ready to send (includes validation check)
hasInvitationReady(): boolean
// Send the actual invitation (with validation)
onShareInvitation(): Promise<void>
// Clear errors when user types
onEmailInput(): void
HTML Template Updates
Email Input with Chip:
<div class="email-field-container">
<!-- Text input state with Angular Reactive Forms -->
<mat-form-field class="email-field" appearance="outline" *ngIf="!emailChip">
<mat-label>Enter email address</mat-label>
<input matInput [formControl]="emailFormControl"
(keydown)="onEmailKeyDown($event)"
type="email">
<!-- Automatic error handling with specific messages -->
<mat-error *ngIf="emailFormControl.hasError('required')">
Email is required
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('email')">
Please enter a valid email address
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('emailExists')">
User is already a member of this project
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('allowlistError')">
{{ emailFormControl.getError('allowlistError')?.message }}
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('serverError')">
{{ emailFormControl.getError('serverError')?.message }}
</mat-error>
</mat-form-field>
<!-- Chip state using real mat-form-field with native error handling -->
<mat-form-field class="email-field chip-state" appearance="outline" *ngIf="emailChip">
<mat-label>Enter email address</mat-label>
<!-- Hidden input for MatFormFieldControl and proper focus handling -->
<input matInput [value]="emailChip" readonly
(keydown)="onChipKeyDown($event)"
type="text"
style="opacity: 0; position: absolute; width: 100%; height: 100%; top: 0; left: 0;">
<!-- Chip display overlay -->
<div class="chip-overlay">
<mat-chip-set>
<mat-chip [class.error-chip]="emailError" (removed)="removeEmailChip()">
{{ emailChip }}
<button matChipRemove><mat-icon>cancel</mat-icon></button>
</mat-chip>
</mat-chip-set>
</div>
<mat-error *ngIf="emailError">{{ emailError }}</mat-error>
</mat-form-field>
</div>
Role Selector:
<mat-form-field class="role-field" appearance="outline">
<mat-label>Role</mat-label>
<mat-select [(value)]="selectedRole">
<mat-option *ngFor="let role of availableRoles"
[value]="role.value"
[matTooltip]="role.description"
matTooltipPosition="right">
{{ role.name }}
</mat-option>
</mat-select>
</mat-form-field>
Smart Buttons:
<div class="right-actions">
<button mat-button (click)="onCancel()">Cancel</button>
<!-- Share button (when invitation ready) -->
<button mat-raised-button
color="primary"
(click)="onShareInvitation()"
*ngIf="hasInvitationReady()">
<mat-icon>share</mat-icon>
Share
</button>
<!-- Done button (when no invitation pending) -->
<button mat-raised-button
color="primary"
(click)="dialogRef.close()"
*ngIf="!hasInvitationReady()">
Done
</button>
</div>
User Experience Benefits
Before (Problems):
- ❌ Unclear when invitation actually sent
- ❌ Had to press Enter to add people
- ❌ No role selection during invitation
- ❌ "Done" button confusing - did it send invites?
After (Solutions):
- ✅ Clear Visual State: Email chips show what will be invited
- ✅ Explicit Action: "Share" button clearly triggers invitation
- ✅ Role Control: User picks role during invitation process
- ✅ Flexible Input: Enter creates chip, backspace/click edits, consistent focus
- ✅ Clean Interface: Role descriptions in tooltips keep dropdown uncluttered
- ✅ Seamless Transitions: Chips appear in same location as text input
- ✅ Visual Consistency: Both states look like proper form fields
- ✅ Height Stability: No layout jumps between text and chip states
- ✅ Perfect Material Design: Both states use real mat-form-field for consistency
- ✅ Keyboard Navigation: Full keyboard support for chip interaction
- ✅ Robust Validation: Prevents invalid emails and duplicates with prominent Material Design error styling
Interaction Examples
Example 1: Successful Invitation
1. User types "john@company.com"
2. User presses Enter → email becomes chip
3. User changes role from "Reader" to "Editor"
4. User clicks "Share" → invitation sent
5. Chip disappears, form resets, member appears in list
Example 2: Correcting Email
1. User types "john@company.co"
2. User presses Enter → email becomes chip
3. User realizes mistake, clicks X on chip
4. Email becomes editable text: "john@company.co"
5. User corrects to "john@company.com"
6. User presses Enter → corrected chip created
Example 3: Cancel During Input
1. User starts typing "jane@company.com"
2. User decides not to invite anyone
3. User clicks "Cancel" → dialog closes without sending
Example 4: Email Validation (Enhanced Material Design)
1. User types "invalid@" (incomplete email)
2. User presses Enter → Material Design error styling appears:
- Red border around form field
- Red floating label text
- Red error text below: "Please enter a valid email address"
3. User continues typing → error styling persists (not cleared immediately)
4. User completes to "invalid@domain.com" → all red styling automatically clears
5. User presses Enter → chip created successfully
Example 5: Share Button Validation (Material Design)
1. User types "test@" (invalid email)
2. User selects role "Editor"
3. User clicks "Share" → Material Design error styling appears:
- Red border around form field
- Red error text: "Please enter a valid email address"
4. User corrects email to "test@company.com" → red styling automatically clears
5. User clicks "Share" → invitation sent successfully
Example 6: Duplicate Prevention
1. User types email of existing member
2. User presses Enter → red error: "User is already a member of this project"
3. User sees existing member in list above
4. User clears field and tries different email
Keyboard Shortcuts
| Key | Action | Context |
|---|---|---|
| Enter | Create email chip | When typing email |
| Backspace | Edit email chip | When input empty + chip exists |
| Backspace/Delete | Unwrap chip to text | When chip form field focused |
| Click (X) | Unwrap chip to text | Click chip remove button |
| Tab | Navigate to role selector | After chip creation |
| Escape | Cancel dialog | Anytime |
Focus Management:
- After Enter: Chip form field auto-focused for immediate keyboard interaction
- After Backspace/Delete: Input field auto-focused with cursor at end of email
- After Click (X): Input field auto-focused with cursor at end of email
- Seamless Flow: No need to click between keyboard or mouse operations
Visual Design
Color Coding:
- Primary Blue: Share button (main action)
- Default Gray: Cancel/Done buttons (secondary actions)
- Role Colors: Chips use existing role color system
Layout:
- Horizontal Layout: Email (2/3 width) + Role (1/3 width)
- Consistent Heights: Both form fields use 64px height with slack space for chips
- No Height Jumps: Pre-allocated space prevents field expansion when chips appear
- Centered Content: Chips and inputs properly centered within form fields
- Visual Hierarchy: Clear separation of input area and member list
- Action Grouping: Copy link on left, main actions on right
Future Enhancements
Phase 2 Additions:
- Multi-email Support: Allow multiple chips for bulk invitations
- Email Suggestions: Autocomplete from company directory
- Invitation Templates: Pre-filled role combinations
- Keyboard Navigation: Arrow keys to navigate between chips
Advanced Features:
- Drag & Drop: Reorder email chips
- Paste Support: Parse multiple emails from clipboard
- Validation: Real-time email domain checking
- Undo/Redo: Invitation history management
Testing Scenarios
Core Functionality:
✅ Type valid email + Enter → creates chip
✅ Type invalid email + Enter → shows error, no chip
✅ Click X on chip → removes chip
✅ Backspace on empty input → edits chip
✅ Change role → updates selection
✅ Click Share with valid email → sends invitation
✅ Click Share with invalid email → shows error
✅ Click Cancel → closes dialog
Edge Cases:
✅ Invalid email format → shows Material Design error
✅ Duplicate member email → shows "already a member" error
✅ Empty input + Enter → shows "Email is required" error
✅ Rapid typing + Enter → only creates one chip if valid
✅ Role change with no email → no effect
✅ Multiple Enter presses → no duplicate chips
✅ Start typing after error → error clears automatically
✅ Remove chip → error clears if present
Implementation Status
✅ Complete Features:
- Email chip creation/removal system with no height jumps
- Comprehensive email validation with Material Design error styling
- Role selector with tooltip descriptions
- Smart button behavior (Done ↔ Share) with validation checks
- Keyboard shortcuts (Enter/Backspace) with validation
- Visual layout improvements with consistent field heights and slack space
- Duplicate member prevention
- Real-time error clearing and user feedback
- Cancel functionality
🚀 Production Ready!
The sharing dialog now provides an intuitive, clear, and professional user experience that eliminates confusion about the invitation process.
Troubleshooting
Common Development Issues
MatFormFieldControl Error:
ERROR Error: mat-form-field must contain a MatFormFieldControl
Cause: Angular Material requires form controls to be direct children of mat-form-field. Nesting inputs inside divs breaks this requirement.
Solution: Separate the chip display from the text input:
- Chips displayed outside of
mat-form-field - Text input as direct child of
mat-form-field - Use conditional rendering (
*ngIf) to switch between states - Add placeholder field to maintain consistent height
Fixed Structure:
<div class="email-field-container">
<!-- Input state -->
<mat-form-field *ngIf="!emailChip">
<input matInput> <!-- Direct child ✅ -->
<mat-error>...</mat-error>
</mat-form-field>
<!-- Chip state using real mat-form-field -->
<mat-form-field class="chip-state" *ngIf="emailChip">
<input matInput readonly (keydown)="onChipKeyDown($event)"
style="opacity: 0; position: absolute; width: 100%; height: 100%;">
<!-- ↑ Hidden but focusable for proper Material focus styling -->
<div class="chip-overlay">
<mat-chip-set>...</mat-chip-set>
</div>
<mat-error>...</mat-error>
</mat-form-field>
</div>
Confusing Chip Display:
Problem: Chip appears above input with separate "Email selected" box below Cause: Separate containers for chip and placeholder create visual disconnect Solution: Style chip container to match form field appearance with proper label positioning
Height Inconsistency:
Problem: Input field jumps in height when text converts to chip Cause: Different padding and sizing between text input and chip container Solution:
- Fixed height of 56px for both states
- Reduced chip padding to fit within height constraint
- Smaller chip size (28px) to allow compact layout
- Error positioning below field boundary to prevent expansion
Inconsistent Material Design Styling:
Problem: Custom chip container styling doesn't match Material form field appearance
Cause: Recreating Material Design styles manually leads to inconsistencies
Solution: Use real mat-form-field for both text and chip states
Key Benefits:
- ✅ Perfect Material Design Consistency: Same focus states, label animations, error styling
- ✅ Proper Focus Behavior: Hidden input receives focus for authentic Material styling
- ✅ Automatic Accessibility: All ARIA attributes and keyboard navigation built-in
- ✅ Zero Style Drift: No custom styles that might become outdated
- ✅ Reduced Maintenance: Leverages Angular Material's robust implementation
Implementation:
<!-- Both states use mat-form-field -->
<mat-form-field class="email-field chip-state" *ngIf="emailChip">
<mat-label>Enter email address</mat-label>
<!-- Hidden input for MatFormFieldControl compliance and proper focus -->
<input matInput [value]="emailChip" readonly
(keydown)="onChipKeyDown($event)"
style="opacity: 0; position: absolute; width: 100%; height: 100%;">
<!-- Chip overlay positioned within form field -->
<div class="chip-overlay">
<mat-chip-set>...</mat-chip-set>
</div>
<mat-error *ngIf="emailError">{{ emailError }}</mat-error>
</mat-form-field>
CSS (Minimal Override):
.email-field.chip-state {
input[readonly] {
cursor: pointer; // Allow focusing
pointer-events: auto; // Enable keyboard events
}
.chip-overlay {
position: absolute;
top: 16px; // Align with input content area
left: 12px;
right: 12px;
z-index: 2; // Above hidden input, below label
}
}
Click (X) Button Focus Issue:
Problem: Clicking the (X) button to remove chip doesn't activate the input field
Cause: removeEmailChip() method lacked focus management logic
Solution: Added same focus logic to removeEmailChip() as used in keyboard unwrap
Implementation:
removeEmailChip(): void {
if (this.emailChip) {
this.newMemberEmail = this.emailChip;
this.emailChip = null;
this.emailError = null;
// Focus the input field after unwrapping (consistent with keyboard behavior)
setTimeout(() => {
const inputElement = document.querySelector('.email-field input') as HTMLInputElement;
if (inputElement) {
inputElement.focus();
try {
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
} catch (error) {
// Graceful degradation for email inputs
}
}
}, 0);
}
}
Result: Both keyboard (Backspace/Delete) and mouse (click X) interactions now provide consistent focus behavior.
Email Validation Error Display Issue:
Problem: Invalid email errors not displayed with proper Material Design styling
Cause: Manual error handling with emailError variable instead of using Angular's form validation system
Solution: Implement Angular Reactive Forms with FormControl and built-in validators for proper Material Design error handling
Before (Manual Error Handling):
// ❌ Manual error state management
emailError: string | null = null;
newMemberEmail = '';
validateEmail(email: string): string | null {
if (!email) return 'Email is required';
if (!this.isValidEmail(email)) return 'Please enter a valid email address';
return null;
}
createEmailChip(): void {
const validationError = this.validateEmail(this.newMemberEmail);
if (validationError) {
this.emailError = validationError; // Manual error setting
return;
}
}
After (Angular Reactive Forms):
// ✅ Angular FormControl with built-in validators
emailFormControl = new FormControl('', [
Validators.required,
Validators.email,
this.customEmailValidator.bind(this)
]);
// Custom validator for business logic only (not email format)
customEmailValidator(control: FormControl): {[key: string]: any} | null {
if (!control.value) return null;
const email = control.value.trim();
// Only check if email is already a member (let Angular's email validator handle format)
const isExistingMember = this.members.some(member =>
member.email.toLowerCase() === email.toLowerCase()
);
if (isExistingMember) {
return { 'emailExists': { value: email } };
}
return null;
}
createEmailChip(): void {
this.emailFormControl.markAsTouched();
if (this.emailFormControl.invalid) {
return; // Errors show automatically via Angular Material
}
}
Validation Triggers:
- ✅ Enter Key: Validates email when creating chip
- ✅ Share Button: Validates email before sending invitation
- ✅ Smart Clearing: Errors persist until email becomes valid or is cleared
- ✅ Non-Intrusive: No aggressive real-time validation while typing
Angular Reactive Forms Template:
<!-- Before: Manual error handling with ngModel -->
<mat-form-field appearance="outline">
<input matInput [(ngModel)]="newMemberEmail" type="email">
<mat-error *ngIf="emailError">{{ emailError }}</mat-error>
</mat-form-field>
<!-- After: FormControl with automatic error handling -->
<mat-form-field appearance="outline">
<mat-label>Enter email address</mat-label>
<input matInput [formControl]="emailFormControl" type="email">
<!-- Multiple specific error messages -->
<mat-error *ngIf="emailFormControl.hasError('required')">
Email is required
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('email')">
Please enter a valid email address
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('emailExists')">
User is already a member of this project
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('allowlistError')">
{{ emailFormControl.getError('allowlistError')?.message }}
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('serverError')">
{{ emailFormControl.getError('serverError')?.message }}
</mat-error>
</mat-form-field>
Minimal CSS (No Custom Error Styling Needed):
.email-field {
// Only disable browser validation styling that interferes
input:invalid {
box-shadow: none;
}
}
Key Benefits:
- 🎯 Proper Angular Approach: Uses Reactive Forms with FormControl (Angular best practice)
- 🎯 Automatic Material Design: Red border, red label, and error text work natively
- 🎯 Built-in Validators: Leverages Angular's
Validators.requiredandValidators.email - 🎯 Custom Validation: Easy to add domain-specific validation (duplicate emails, etc.)
- 🎯 Framework Integration: Perfect integration with Angular Material components
- 🎯 Declarative Errors: Each error type has its own specific message
- 🎯 Less Code: No manual error state management, Angular handles it automatically
- 🎯 Proper Timing: Errors show when field is touched and invalid (industry standard)
- 🎯 Server Error Integration: Backend errors (Google Group, allowlist, etc.) also use Material Design styling
Server Error Integration:
Problem: Google Group allowlist errors and other backend errors displayed as generic red messages at top of dialog Cause: Server errors handled separately from form validation, inconsistent with Material Design patterns Solution: Integrate server errors into FormControl error system for consistent Material Design styling
Before (Generic Error Display):
<div *ngIf="error" class="error-message">
{{ error }}
<button mat-button color="primary" (click)="loadProjectMembers()">Retry</button>
</div>
After (Material Design Integration):
// Set server errors on FormControl
setServerError(errorType: string, message: string): void {
const currentErrors = this.emailFormControl.errors || {};
currentErrors[errorType] = { message };
this.emailFormControl.setErrors(currentErrors);
this.emailFormControl.markAsTouched();
}
// Error handling in invitation logic
if (errorMessage.includes('allowlist') || errorMessage.includes('Google Group')) {
this.setServerError('allowlistError', errorMessage);
} else {
this.setServerError('serverError', errorMessage);
}
<!-- Material Design error display -->
<mat-error *ngIf="emailFormControl.hasError('allowlistError')">
{{ emailFormControl.getError('allowlistError')?.message }}
</mat-error>
<mat-error *ngIf="emailFormControl.hasError('serverError')">
{{ emailFormControl.getError('serverError')?.message }}
</mat-error>
Key Benefits:
- ✅ Consistent Styling: All errors use same Material Design red styling
- ✅ Contextual Display: Errors appear directly under the related input field
- ✅ Smart Clearing: Server errors clear when user starts typing or creates new chip
- ✅ Error Type Classification: Different server error types can have different handling
- ✅ FormControl Integration: Works seamlessly with existing validation system
Reactive Forms Disabled State Warning:
Problem: Angular warning about using [disabled] attribute with reactive forms: "It looks like you're using the disabled attribute with a reactive form directive"
Cause: Using template [disabled]="condition" on FormControl-bound inputs instead of managing disabled state through the FormControl itself
Solution: Manage disabled state through FormControl methods rather than template attributes
Before (Template-based Disabled State):
<!-- ❌ Warning: disabled attribute with FormControl -->
<input matInput [formControl]="emailFormControl" [disabled]="!isOwner || loading">
After (FormControl-managed Disabled State):
// Initialize FormControl with disabled state
emailFormControl = new FormControl(
{ value: '', disabled: true }, // Start disabled
[Validators.required, Validators.email, this.customEmailValidator.bind(this)]
);
// Method to update disabled state
updateEmailFormDisabledState(): void {
const shouldBeDisabled = !this.isOwner || this.loading;
if (shouldBeDisabled && this.emailFormControl.enabled) {
this.emailFormControl.disable();
} else if (!shouldBeDisabled && this.emailFormControl.disabled) {
this.emailFormControl.enable();
}
}
// Call when state changes
async ngOnInit(): Promise<void> {
// ... determine permissions ...
this.updateEmailFormDisabledState();
}
<!-- ✅ Clean: FormControl manages disabled state -->
<input matInput [formControl]="emailFormControl">
Key Benefits:
- ✅ No Angular Warnings: Eliminates reactive forms disabled attribute warnings
- ✅ Prevents Change Detection Issues: Avoids "changed after checked" errors
- ✅ Angular Best Practice: Uses recommended FormControl state management
- ✅ Centralized State: Disabled logic controlled in one place
- ✅ Reactive: Updates automatically when
isOwnerorloadingchanges
Focus State Issue:
Problem: Two different focus appearances - proper bright blue vs dark misaligned outline
Cause: Attempting to focus mat-form-field container directly instead of its input control
Solution: Focus the hidden input element, which triggers proper Material Design focus styling
Critical Fix:
// ❌ Wrong: Focus the form field container
const chipContainer = document.querySelector('.email-field.chip-state') as HTMLElement;
chipContainer.focus(); // Creates improper focus styling
// ✅ Correct: Focus the hidden input
const chipInput = document.querySelector('.email-field.chip-state input') as HTMLInputElement;
chipInput.focus(); // Triggers proper Material Design focus
setSelectionRange Error:
Problem: InvalidStateError: Failed to execute 'setSelectionRange' on 'HTMLInputElement': The input element's type ('email') does not support selection
Cause: Email input types don't support text selection operations per HTML specification
Solution: Wrap setSelectionRange in try-catch block for graceful degradation
onChipKeyDown(event: KeyboardEvent): void {
if (event.key === 'Backspace' || event.key === 'Delete') {
// ... unwrap chip logic ...
setTimeout(() => {
const inputElement = document.querySelector('.email-field input') as HTMLInputElement;
if (inputElement) {
inputElement.focus();
// Try to set cursor position - gracefully handle unsupported input types
try {
inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
} catch (error) {
// Silently ignore - setSelectionRange not supported on email inputs
}
}
}, 0);
}
}