improving-week-36 #1
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user