improving-week-36 #1

Merged
jare2473 merged 41 commits from improving-week-36 into main 2025-09-04 10:49:05 +02:00
2 changed files with 108 additions and 8 deletions
Showing only changes of commit e604bb21b1 - Show all commits

View File

@@ -7,6 +7,7 @@ export function ParticipantsSelector() {
const booking = useBookingContext();
const [searchTerm, setSearchTerm] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const [recentSearches, setRecentSearches] = useState([
{ id: 1, name: 'Arjohn Emilsson', email: 'arjohn.emilsson@dsv.su.se' },
{ id: 3, name: 'Hedvig Engelmark', email: 'hedvig.engelmark@dsv.su.se' },
@@ -29,6 +30,9 @@ export function ParticipantsSelector() {
// Helper function to check if person is already selected
const isPersonSelected = (personName) => booking.participants.includes(personName);
// Get all available options for keyboard navigation
const allOptions = showRecentSearches ? recentSearches : (showAllPeople ? displayPeople : []);
useEffect(() => {
const handleClickOutside = (event) => {
@@ -42,12 +46,60 @@ export function ParticipantsSelector() {
}, []);
const handleInputFocus = () => {
// Don't auto-open dropdown on focus - wait for user interaction
setFocusedIndex(-1);
};
const handleInputClick = () => {
setIsDropdownOpen(true);
setFocusedIndex(-1);
};
const handleInputChange = (e) => {
setSearchTerm(e.target.value);
setIsDropdownOpen(true);
setFocusedIndex(-1);
};
const handleKeyDown = (e) => {
if (!isDropdownOpen) {
// When dropdown is closed, Enter should open it
if (e.key === 'Enter') {
e.preventDefault();
setIsDropdownOpen(true);
setFocusedIndex(-1);
}
return;
}
switch (e.key) {
case 'ArrowDown':
case 'Tab':
e.preventDefault();
setFocusedIndex(prev =>
prev < allOptions.length - 1 ? prev + 1 : 0
);
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex(prev =>
prev > 0 ? prev - 1 : allOptions.length - 1
);
break;
case 'Enter':
e.preventDefault();
if (focusedIndex >= 0 && focusedIndex < allOptions.length) {
handleSelectPerson(allOptions[focusedIndex]);
}
break;
case 'Escape':
e.preventDefault();
setIsDropdownOpen(false);
setFocusedIndex(-1);
// Keep input focused with outline, don't blur completely
inputRef.current?.focus();
break;
}
};
const handleSelectPerson = (person) => {
@@ -57,7 +109,9 @@ export function ParticipantsSelector() {
if (isPersonSelected(person.name)) {
setSearchTerm('');
setIsDropdownOpen(false);
inputRef.current?.blur();
setFocusedIndex(-1);
// Keep focus on input instead of blurring
inputRef.current?.focus();
return;
}
@@ -70,7 +124,9 @@ export function ParticipantsSelector() {
setSearchTerm('');
setIsDropdownOpen(false);
inputRef.current?.blur();
setFocusedIndex(-1);
// Keep focus on input for continued interaction
inputRef.current?.focus();
};
const handleRemoveParticipant = (participantToRemove) => {
@@ -89,22 +145,35 @@ export function ParticipantsSelector() {
value={searchTerm}
onChange={handleInputChange}
onFocus={handleInputFocus}
onClick={handleInputClick}
onKeyDown={handleKeyDown}
placeholder="Search for participants..."
className={styles.searchInput}
role="combobox"
aria-expanded={isDropdownOpen}
aria-autocomplete="list"
aria-activedescendant={focusedIndex >= 0 ? `option-${focusedIndex}` : undefined}
/>
{/* Dropdown */}
{isDropdownOpen && (
<div className={styles.dropdown}>
<div
className={styles.dropdown}
role="listbox"
aria-label="Participant suggestions"
>
{/* Recent Searches */}
{showRecentSearches && (
<div className={styles.section}>
<div className={styles.sectionHeader}>Senaste sökningar</div>
{recentSearches.map((person) => (
{recentSearches.map((person, index) => (
<div
key={`recent-${person.id}`}
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''}`}
id={`option-${index}`}
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`}
onClick={() => handleSelectPerson(person)}
role="option"
aria-selected={isPersonSelected(person.name)}
>
<div className={styles.personInfo}>
<div className={styles.personName}>
@@ -123,11 +192,14 @@ export function ParticipantsSelector() {
displayPeople.length > 0 ? (
<div className={styles.section}>
<div className={styles.sectionHeader}>Sökresultat</div>
{displayPeople.map((person) => (
{displayPeople.map((person, index) => (
<div
key={person.id}
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''}`}
id={`option-${index}`}
className={`${styles.dropdownItem} ${isPersonSelected(person.name) ? styles.selectedItem : ''} ${index === focusedIndex ? styles.focusedItem : ''}`}
onClick={() => handleSelectPerson(person)}
role="option"
aria-selected={isPersonSelected(person.name)}
>
<div className={styles.personInfo}>
<div className={styles.personName}>
@@ -158,8 +230,11 @@ export function ParticipantsSelector() {
<button
className={styles.removeButton}
onClick={() => handleRemoveParticipant(participant)}
onFocus={(e) => e.target.closest(`.${styles.participantChip}`).classList.add(styles.chipFocused)}
onBlur={(e) => e.target.closest(`.${styles.participantChip}`).classList.remove(styles.chipFocused)}
type="button"
title="Remove participant"
title={`Remove ${participant}`}
aria-label={`Remove ${participant} from participants`}
>
×
</button>

View File

@@ -66,6 +66,17 @@
transform: scale(1.1);
}
.removeButton:focus {
outline: 2px solid var(--focus-ring-color, #3B82F6);
outline-offset: 2px;
background-color: rgba(37, 99, 235, 0.3);
}
.chipFocused {
box-shadow: 0 0 0 2px var(--focus-ring-color, #3B82F6) !important;
border-color: var(--focus-ring-color, #3B82F6) !important;
}
.searchContainer {
position: relative;
width: 100%;
@@ -91,6 +102,7 @@
.searchInput:focus {
outline: 2px solid var(--focus-ring-color, #3e70ec);
outline-offset: -1px;
border-color: var(--focus-ring-color, #3e70ec);
}
.dropdown {
@@ -191,6 +203,19 @@
margin-left: 0.5rem;
}
.focusedItem {
background-color: #3B82F6 !important;
color: white !important;
}
.focusedItem .personEmail {
color: rgba(255, 255, 255, 0.8) !important;
}
.focusedItem .selectedIndicator {
color: white !important;
}
.noResults {
padding: 1rem;
text-align: center;