Compare commits
2 commits
f3776b86b0
...
54db215848
| Author | SHA1 | Date | |
|---|---|---|---|
| 54db215848 | |||
| 45f07f763f |
5 changed files with 271 additions and 30 deletions
22
UI_UPDATE.MD
22
UI_UPDATE.MD
|
|
@ -461,14 +461,22 @@ All utilities follow the established pattern of using CSS custom properties from
|
|||
### Sub-task 4.2: Interactive States and Feedback
|
||||
|
||||
#### 4.2.1: State Implementation
|
||||
- [ ] Design comprehensive hover states
|
||||
- [ ] Implement focus indicators meeting WCAG standards
|
||||
- [ ] Add active and pressed state styling
|
||||
- [x] Design comprehensive hover states
|
||||
- [x] Implement focus indicators meeting WCAG standards
|
||||
- [x] Add active and pressed state styling
|
||||
|
||||
#### 4.2.2: Form Validation
|
||||
- [ ] Implement visual form validation feedback
|
||||
- [ ] Add error state styling
|
||||
- [ ] Create success confirmation states
|
||||
#### 4.2.2: Form Validation - COMPLETED 9/29/2025
|
||||
- [x] Implement visual form validation feedback
|
||||
- [x] Add error state styling
|
||||
- [x] Create success confirmation states
|
||||
|
||||
**Notes:** Comprehensive form validation system implemented for chat inputs including:
|
||||
- HTML structure updates: Added form-group wrappers with error message elements for nickname and message inputs
|
||||
- Enhanced JavaScript validation: Added client-side validation with detailed error messages for nickname (1-20 chars, alphanumeric/places) and message (1-1000 chars) requirements
|
||||
- CSS integration: Utilizes existing .form-group--error and .form-group--success classes for visual feedback
|
||||
- User experience: Error messages display below inputs, success states provide green styling and toast confirmation, focus management ensures good UX flow
|
||||
- Validation coverage: Empty inputs, character length limits, nickname character restrictions, and server error handling
|
||||
- Accessibility: Proper error messaging and focus management for screen readers and keyboard navigation
|
||||
|
||||
### Sub-task 4.3: Enhanced User Feedback Systems
|
||||
|
||||
|
|
|
|||
|
|
@ -76,44 +76,137 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Validation helper functions
|
||||
function validateNickname(nickname) {
|
||||
if (!nickname) {
|
||||
return { valid: false, message: 'Nickname is required' };
|
||||
}
|
||||
|
||||
const trimmed = nickname.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return { valid: false, message: 'Nickname cannot be empty' };
|
||||
}
|
||||
|
||||
if (trimmed.length > 20) {
|
||||
return { valid: false, message: 'Nickname must be 20 characters or less' };
|
||||
}
|
||||
|
||||
if (trimmed.length < 1) {
|
||||
return { valid: false, message: 'Nickname must be at least 1 character' };
|
||||
}
|
||||
|
||||
// Allow alphanumeric characters and spaces, apostrophes, hyphens
|
||||
const nicknameRegex = /^[a-zA-Z0-9\s'-]+$/;
|
||||
if (!nicknameRegex.test(trimmed)) {
|
||||
return { valid: false, message: 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function validateMessage(message) {
|
||||
if (!message) {
|
||||
return { valid: false, message: 'Message is required' };
|
||||
}
|
||||
|
||||
const trimmed = message.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return { valid: false, message: 'Message cannot be empty' };
|
||||
}
|
||||
|
||||
if (trimmed.length > 1000) {
|
||||
return { valid: false, message: 'Message must be 1000 characters or less' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function updateValidationUI(field, errorElement, isValid, errorMessage) {
|
||||
const formGroup = errorElement.closest('.form-group');
|
||||
const input = formGroup.querySelector('input');
|
||||
|
||||
formGroup.classList.remove('form-group--error', 'form-group--success');
|
||||
|
||||
if (isValid) {
|
||||
formGroup.classList.add('form-group--success');
|
||||
errorElement.style.display = 'none';
|
||||
} else {
|
||||
formGroup.classList.add('form-group--error');
|
||||
errorElement.textContent = errorMessage;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function clearValidationState(field) {
|
||||
const formGroup = document.querySelector(`#${field}`).closest('.form-group');
|
||||
if (formGroup) {
|
||||
formGroup.classList.remove('form-group--error', 'form-group--success');
|
||||
const errorElement = document.getElementById(`${field}Error`);
|
||||
if (errorElement) {
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send message functionality
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const nicknameInput = document.getElementById('nickname');
|
||||
const nicknameErrorElement = document.getElementById('nicknameError');
|
||||
const messageErrorElement = document.getElementById('messageError');
|
||||
|
||||
const message = messageInput?.value?.trim() || '';
|
||||
const nickname = nicknameInput?.value?.trim() || '';
|
||||
|
||||
if (!nickname) {
|
||||
if (nicknameInput) {
|
||||
nicknameInput.style.borderColor = 'var(--dodgers-red)';
|
||||
nicknameInput.focus();
|
||||
}
|
||||
UIControls.showToast('Please enter a nickname');
|
||||
setTimeout(() => {
|
||||
if (nicknameInput) nicknameInput.style.borderColor = '#e0e0e0';
|
||||
}, 2000);
|
||||
// Validate nickname
|
||||
const nicknameValidation = validateNickname(nickname);
|
||||
updateValidationUI('nickname', nicknameErrorElement, nicknameValidation.valid, nicknameValidation.message);
|
||||
|
||||
// Validate message
|
||||
const messageValidation = validateMessage(message);
|
||||
updateValidationUI('messageInput', messageErrorElement, messageValidation.valid, messageValidation.message);
|
||||
|
||||
// If either validation failed, focus the appropriate field and stop
|
||||
if (!nicknameValidation.valid) {
|
||||
nicknameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
return; // Just return quietly for empty message
|
||||
if (!messageValidation.valid) {
|
||||
messageInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send message via API
|
||||
// All validation passed - send message via API
|
||||
API.sendMessage(nickname, message)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Clear the form and show success
|
||||
if (messageInput) messageInput.value = '';
|
||||
AppState.nickname = nickname;
|
||||
// Message will be added automatically by polling
|
||||
|
||||
// Show success confirmation
|
||||
updateValidationUI('nickname', nicknameErrorElement, true);
|
||||
updateValidationUI('messageInput', messageErrorElement, true);
|
||||
|
||||
UIControls.showToast('Message sent successfully!');
|
||||
|
||||
// Clear success state after a delay
|
||||
setTimeout(() => {
|
||||
clearValidationState('nickname');
|
||||
clearValidationState('messageInput');
|
||||
}, 3000);
|
||||
|
||||
AppLogger.log('Message sent successfully');
|
||||
} else if (data.error) {
|
||||
UIControls.showToast(data.error);
|
||||
// Show server error
|
||||
updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message: ' + data.error);
|
||||
UIControls.updateConnectionStatus(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
AppLogger.error('Error sending message:', error);
|
||||
UIControls.showToast('Failed to send message');
|
||||
updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message. Please try again.');
|
||||
UIControls.updateConnectionStatus(false);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
16
index.php
16
index.php
|
|
@ -461,8 +461,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||
<div class="chat__user-id-display">
|
||||
Your ID: <span class="chat__user-id-badge" id="userId">Loading...</span>
|
||||
</div>
|
||||
<div class="chat__nickname-input">
|
||||
<input type="text" id="nickname" placeholder="Choose a nickname..." maxlength="20" autocomplete="off" aria-label="Enter your nickname">
|
||||
<div class="form-group">
|
||||
<div class="chat__nickname-input">
|
||||
<input type="text" id="nickname" placeholder="Choose a nickname..." maxlength="20" autocomplete="off" aria-label="Enter your nickname">
|
||||
</div>
|
||||
<div class="form-error" id="nicknameError" style="display: none;"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -481,9 +484,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||
|
||||
<section class="chat__input" aria-labelledby="chat-input-heading">
|
||||
<h3 id="chat-input-heading" class="sr-only">Send Message</h3>
|
||||
<div class="chat__input-group">
|
||||
<input type="text" id="messageInput" placeholder="Type a message..." maxlength="1000" autocomplete="off" aria-label="Type your message">
|
||||
<button data-action="send-message" aria-label="Send message">Send</button>
|
||||
<div class="form-group">
|
||||
<div class="chat__input-group">
|
||||
<input type="text" id="messageInput" placeholder="Type a message..." maxlength="1000" autocomplete="off" aria-label="Type your message">
|
||||
<button data-action="send-message" aria-label="Send message">Send</button>
|
||||
</div>
|
||||
<div class="form-error" id="messageError" style="display: none;"></div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -123,10 +123,11 @@
|
|||
background: rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
/* Focus states for accessibility */
|
||||
/* Focus states for accessibility - WCAG AA compliant */
|
||||
.btn:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 1px var(--color-surface), 0 0 0 4px var(--dodgers-blue-100);
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
|
|
@ -225,8 +226,68 @@
|
|||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 2px solid var(--color-focus);
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px var(--dodgers-blue-100);
|
||||
}
|
||||
|
||||
/* Form focus states - WCAG AA compliant */
|
||||
.form-control:focus,
|
||||
.form-input:focus,
|
||||
.form-textarea:focus,
|
||||
.form-select:focus {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px var(--dodgers-blue-100);
|
||||
border-color: var(--color-focus);
|
||||
}
|
||||
|
||||
.chat__input input:focus {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px var(--dodgers-blue-100);
|
||||
}
|
||||
|
||||
/* Link focus states */
|
||||
a:focus-visible {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Dashboard and control focus states */
|
||||
.dashboard__toggle-btn:focus-visible {
|
||||
outline: 3px solid var(--dodgers-white);
|
||||
outline-offset: 2px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.video-player__quality-selector:focus-visible,
|
||||
.video-player__toggle-chat-btn:focus-visible,
|
||||
.stream-stats__refresh-btn:focus-visible {
|
||||
outline: 3px solid var(--dodgers-white);
|
||||
outline-offset: 2px;
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
/* Chat admin controls focus */
|
||||
.chat__admin-btn:focus-visible {
|
||||
outline: 3px solid var(--dodgers-gold);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Mobile navigation focus */
|
||||
.mobile-nav-btn:focus-visible {
|
||||
outline: 3px solid var(--dodgers-white);
|
||||
outline-offset: 2px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* FAB focus */
|
||||
.fab:focus-visible {
|
||||
outline: 3px solid var(--dodgers-white);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 1px var(--dodgers-blue-400), var(--elevation-5);
|
||||
}
|
||||
|
||||
/* Card variants */
|
||||
|
|
@ -1754,6 +1815,78 @@
|
|||
|
||||
.chat__input button:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: var(--elevation-1);
|
||||
}
|
||||
|
||||
/* Active states for links and form elements */
|
||||
a:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Form elements pressed states */
|
||||
.form-control:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.form-input:active,
|
||||
.form-textarea:active,
|
||||
.form-select:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 2px var(--dodgers-blue-200);
|
||||
}
|
||||
|
||||
/* Dashboard controls active states */
|
||||
.dashboard__toggle-btn:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.stats-card:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--elevation-2);
|
||||
}
|
||||
|
||||
/* Video controls pressed states */
|
||||
.video-player__quality-selector:active,
|
||||
.video-player__toggle-chat-btn:active,
|
||||
.stream-stats__refresh-btn:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
/* Chat admin controls active states */
|
||||
.chat__admin-btn:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,215,0,0.6);
|
||||
}
|
||||
|
||||
/* Message actions pressed states */
|
||||
.delete-btn:active, .ban-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Mobile navigation active states */
|
||||
.mobile-nav-btn:active {
|
||||
transform: scale(0.95) translateY(0);
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
/* FAB active states */
|
||||
.fab:active, .fab.secondary:active {
|
||||
transform: scale(1.05);
|
||||
box-shadow: var(--elevation-3);
|
||||
}
|
||||
|
||||
/* Toast notifications active states */
|
||||
.toast:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
/* Activity and user item active states */
|
||||
.activity-item:active,
|
||||
.user-item:active {
|
||||
transform: scale(0.98);
|
||||
background: var(--color-surface-variant);
|
||||
}
|
||||
|
||||
.chat__empty-state {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@
|
|||
--hover-overlay: rgba(255, 255, 255, 0.1);
|
||||
--active-overlay: rgba(255, 255, 255, 0.2);
|
||||
--focus-ring: rgba(0, 90, 156, 0.5);
|
||||
--color-focus: var(--dodgers-blue-400); /* WCAG AA compliant focus color */
|
||||
|
||||
/* Borders & Dividers */
|
||||
--border-color: #1a2332;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue