Skip to content

Commit

Permalink
Merge pull request #468 from Lemoncode/feature/#441-Make-edit-relatio…
Browse files Browse the repository at this point in the history
…n-accesible

[READY_TO_REVIEW]accessible dropdown component
  • Loading branch information
brauliodiez authored Apr 30, 2024
2 parents 2752e92 + 41ecea1 commit 16b7f8f
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface Props {
optionsListVisible: boolean;
handleOptionClick: (option: DropdownOptionVm) => void;
selectedOption?: string;
modalRef: React.RefObject<HTMLUListElement>;
label?: string;
}

export const OptionGroup: React.FC<Props> = props => {
Expand All @@ -19,12 +21,19 @@ export const OptionGroup: React.FC<Props> = props => {
optionsListVisible,
handleOptionClick,
selectedOption,
modalRef,
label,
} = props;

return (
<ul
className={classes.options}
style={{ display: optionsListVisible ? 'block' : 'none' }}
ref={modalRef}
onClick={e => e.stopPropagation()}
id={`${name}-select`}
role="listbox"
aria-label={label}
>
<Option
name={name}
Expand All @@ -44,22 +53,20 @@ interface OptionProps {
}

const Option: React.FC<OptionProps> = props => {
const { options, selectedOption, name, handleOptionClick } = props;
const { options, selectedOption, handleOptionClick, name } = props;

return options.map(option => (
<li key={option.id}>
<li
key={option.id}
role="option"
onClick={() => handleOptionClick(option)}
aria-selected={selectedOption === option.id}
id={`${name}-option-${option.id}`}
>
{option.label}
<div className={classes.svg}>
{selectedOption === option.id ? <Tick /> : ''}
</div>
<label>
<input
type="radio"
name={name}
value={option.id}
onChange={() => handleOptionClick(option)}
/>
{option.label}
</label>
</li>
));
};
48 changes: 48 additions & 0 deletions src/common/components/dropdown/dropdown.business.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const handleFocus = (
previousFocusedElement: React.MutableRefObject<Element | null>,
containerRef: React.RefObject<HTMLUListElement>
) => {
previousFocusedElement.current = document.activeElement;
containerRef.current?.focus();

if (containerRef.current) {
containerRef.current.tabIndex = -1;
}

const focusableElementsInside =
containerRef.current?.querySelectorAll('li[role="option"]');

if (focusableElementsInside && focusableElementsInside.length > 0) {
const firstElement = focusableElementsInside[0];
if (firstElement instanceof HTMLElement) {
firstElement.setAttribute('tabindex', '0');
firstElement.focus();
}

focusableElementsInside.forEach((element: Element) => {
if (element.getAttribute('aria-selected') === 'true') {
const selectedElement = element;
if (selectedElement instanceof HTMLElement) {
selectedElement.setAttribute('tabindex', '0');
selectedElement.focus();
}
}
});
}
};

export const handleNextFocus = (
previousFocusedElement: React.MutableRefObject<Element | null>
) => {
const focusableElements = document.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"]'
);
focusableElements.forEach((element: Element) => {
element.removeAttribute('tabindex');
});
const currentElement = previousFocusedElement.current as HTMLElement;
if (currentElement) {
currentElement.focus();
currentElement.tabIndex = 0;
}
};
44 changes: 21 additions & 23 deletions src/common/components/dropdown/dropdown.component.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
background-color: var(--bg-input);
border-radius: var(--border-radius-xs);
transition: all 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-xs);
}

.select-chosen {
Expand All @@ -22,17 +26,19 @@
}

.options {
--border-select-width: 1px;
--border-select-color: var(--input-border-color-active);
list-style: none;
padding: 8px;
padding-left: 36px;
margin: 0;
display: none;
position: absolute;
z-index: 2;
top: calc(100% + var(--border-select-width));
z-index: 3;
top: calc(0% + var(--border-select-width));
left: calc(0px - var(--border-select-width));
width: calc(100% + var(--border-select-width) * 2);
border: var(--border-select-width) solid var(--border-select-color);
border: var(--border-select-width) solid var(--input-border-color-active);
border-top: none;
background-color: var(--bg-input);
border-radius: var(--border-radius-xs);
Expand Down Expand Up @@ -63,28 +69,15 @@

.options li {
font-size: 16px;
color: var(--text-disabled);
}

.options label {
cursor: pointer;
color: var(--text-color);
}

.options label:hover {
color: var(--input-border-color-active);
}

.options input[type='radio'] {
position: relative;
z-index: -1;
width: 0;
margin: 0;
cursor: pointer;
}

.svg {
position: absolute;
left: 8px;
top: 0;
left: -28px;
display: flex;
align-items: flex-end;
justify-content: center;
Expand All @@ -95,10 +88,15 @@

/*Veil works because it is being used within a modal*/
.veil {
width: 100vw;
height: 100vh;
position: absolute;
z-index: 1;
z-index: 2;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--veil-modal);
opacity: 0.5;
}
102 changes: 81 additions & 21 deletions src/common/components/dropdown/dropdown.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { OptionGroup } from './components/option-group.component';
import { ExpandDown } from '../icons/expand-down-icon.component';
import { DropdownOptionVm } from './dropdown.model';
import { SELECT_AN_OPTION } from './dropdown.const';
import { handleFocus, handleNextFocus } from './dropdown.business';

interface Props {
name: string;
Expand All @@ -13,10 +14,11 @@ interface Props {
selectTitle?: string;
//TODO: css class?
isError?: boolean;
label?: string;
}

export const Dropdown: React.FC<Props> = props => {
const { name, options, value, selectTitle, onChange, isError } = props;
const { name, options, value, selectTitle, onChange, isError, label } = props;
const [optionsListVisible, setOptionsListVisible] = React.useState(false);

const [selectedPath, setSelectedPath] = React.useState(value?.label);
Expand All @@ -29,34 +31,92 @@ export const Dropdown: React.FC<Props> = props => {
setOptionsListVisible(false);
onChange(option);
};
const modalRef = React.useRef<HTMLUListElement>(null);
const selectRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setOptionsListVisible(false);
}
};
const handleFocusOut = (event: FocusEvent) => {
if (
optionsListVisible &&
modalRef.current &&
!modalRef.current.contains(event.relatedTarget as Node)
) {
setOptionsListVisible(false);
}
};

const handleClickOutside = (event: MouseEvent) => {
if (
modalRef.current &&
!modalRef.current.contains(event.target as Node)
) {
setOptionsListVisible(false);
}
};

if (optionsListVisible) {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('focusout', handleFocusOut);
handleFocus(selectRef, modalRef);
const activeID = `${name}-option-${currentSelectedKeyFieldId}`;
selectRef.current?.setAttribute('aria-activedescendant', activeID);
} else {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleClickOutside);
handleNextFocus(selectRef);
selectRef.current?.setAttribute('aria-activedescendant', '');
}

return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('focusout', handleFocusOut);
};
}, [optionsListVisible]);

return (
<>
<div
className={`${classes.selectSelect} ${isError && classes.selectError} ${optionsListVisible && classes.selectActive}`}
ref={selectRef}
role="combobox"
aria-haspopup="listbox"
aria-expanded={optionsListVisible}
aria-live="polite"
aria-controls={`${name}-select`}
aria-activedescendant=""
onClick={() => {
setOptionsListVisible(!optionsListVisible);
}}
aria-label={label}
>
<div
className={classes.selectChosen}
onClick={() => setOptionsListVisible(!optionsListVisible)}
>
<p className={classes.selectText}>
{selectedPath || selectTitle || SELECT_AN_OPTION}
</p>
<ExpandDown />
</div>
<OptionGroup
name={name}
options={options}
optionsListVisible={optionsListVisible}
handleOptionClick={handleOptionClick}
selectedOption={currentSelectedKeyFieldId}
/>
<p className={classes.selectText}>
{selectedPath || selectTitle || SELECT_AN_OPTION}
</p>
<ExpandDown />
</div>
{optionsListVisible && (
<div
className={classes.veil}
onClick={() => setOptionsListVisible(!optionsListVisible)}
></div>
<>
<div
className={classes.veil}
onClick={() => setOptionsListVisible(!optionsListVisible)}
></div>
<OptionGroup
name={name}
options={options}
optionsListVisible={optionsListVisible}
handleOptionClick={handleOptionClick}
selectedOption={currentSelectedKeyFieldId}
modalRef={modalRef}
label={label}
/>
</>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const DropdownFormik: React.FC<DropDownFormikProps> = props => {
formik.setFieldValue('toFieldId', { id: '', label: '' });
}
}}
label={label}
></Dropdown>
{isError && <span className={classes.error}>{meta.error}</span>}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
}

.select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
Expand Down
8 changes: 4 additions & 4 deletions src/common/components/modal-dialog/modal.dialog.bussines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const handleTabsInsideDialog = (
containerRef: React.RefObject<HTMLDivElement>
) => {
const focusableElements = containerRef.current?.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, div[role="combobox"]'
);
const lastFocusableElement =
focusableElements?.[focusableElements.length - 1];
Expand Down Expand Up @@ -32,13 +32,13 @@ export const handleFocus = (
previousFocusedElement.current = document.activeElement;
containerRef.current?.focus();
const focusableElements = document.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, div[role="combobox"]'
);
focusableElements.forEach((element: Element) => {
element.setAttribute('tabindex', '-1');
});
const focusableElementsInside = containerRef.current?.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, div[role="combobox"]'
);
focusableElementsInside?.forEach((element: Element) => {
if (element.ariaHidden !== 'true') {
Expand All @@ -51,7 +51,7 @@ export const handleNextFocus = (
previousFocusedElement: React.MutableRefObject<Element | null>
) => {
const focusableElements = document.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, div[role="combobox"]'
);
focusableElements.forEach((element: Element) => {
element.removeAttribute('tabindex');
Expand Down
2 changes: 1 addition & 1 deletion src/pods/edit-relation/edit-relation.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const EditRelationComponent: React.FC<Props> = props => {
<>
<DropdownFormik
name="type"
label="Type"
label="Type of relation"
options={relationsTypeOptions}
selectTitle="Select type"
></DropdownFormik>
Expand Down

0 comments on commit 16b7f8f

Please sign in to comment.