Skip to main content

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-field for 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

KeyActionContext
EnterCreate email chipWhen typing email
BackspaceEdit email chipWhen input empty + chip exists
Backspace/DeleteUnwrap chip to textWhen chip form field focused
Click (X)Unwrap chip to textClick chip remove button
TabNavigate to role selectorAfter chip creation
EscapeCancel dialogAnytime

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.required and Validators.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 isOwner or loading changes

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);
}
}