Skip to content

Commit

Permalink
feat: add hook useFocusWithin (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Jul 11, 2023
1 parent f1c6b58 commit 5a3cf93
Show file tree
Hide file tree
Showing 14 changed files with 607 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/components/Popup/Popup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ $transition-distance: 10px;

z-index: 1000;
visibility: hidden;
outline: none;

&_open,
&_exit_active {
Expand Down
1 change: 1 addition & 0 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export function Popup({
{...attributes.popper}
{...containerProps}
className={b({open}, className)}
tabIndex={-1}
data-qa={qa}
id={id}
role={role}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
| filterable | `boolean` | `false` | Indicates that select popup have filter section |
| disabled | `boolean` | `false` | Indicates that the user cannot interact with the control |
| hasClear | `boolean` | `false` | Enable displaying icon for clear selected options |
| onFocus | `function` | `-` | Handler that is called when the element receives focus. |
| onBlur | `function` | `-` | Handler that is called when the element loses focus. |

---

Expand Down
17 changes: 13 additions & 4 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type {List} from '../List';
import {KeyCode} from '../constants';
import {useMobile} from '../mobile';
import type {CnMods} from '../utils/cn';
import {useFocusWithin} from '../utils/interactions';
import {useForkRef} from '../utils/useForkRef';
import {useOnFocusOutside} from '../utils/useOnFocusOutside';
import {useSelect} from '../utils/useSelect';

import {EmptyOptions, SelectControl, SelectFilter, SelectList, SelectPopup} from './components';
Expand Down Expand Up @@ -201,14 +201,23 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
}

const handleClose = React.useCallback(() => toggleOpen(false), [toggleOpen]);
const {onFocus, onBlur} = useOnFocusOutside({enabled: open, onFocusOutside: handleClose});
const {onFocus, onBlur} = props;
const {focusWithinProps} = useFocusWithin({
onFocusWithin: onFocus,
onBlurWithin: React.useCallback(
(e: React.FocusEvent) => {
onBlur?.(e);
handleClose();
},
[handleClose, onBlur],
),
});

return (
<div
ref={controlWrapRef}
className={selectBlock(mods, className)}
onFocus={onFocus}
onBlur={onBlur}
{...focusWithinProps}
style={inlineStyles}
>
<SelectControl
Expand Down
10 changes: 4 additions & 6 deletions src/components/Select/components/SelectControl/SelectControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import isEmpty from 'lodash/isEmpty';

import {Icon} from '../../../Icon';
import type {CnMods} from '../../../utils/cn';
import {useForkRef} from '../../../utils/useForkRef';
import {selectControlBlock, selectControlButtonBlock} from '../../constants';
import type {
SelectProps,
Expand Down Expand Up @@ -36,7 +35,7 @@ type ControlProps = {
hasClear?: boolean;
} & Omit<SelectRenderControlProps, 'onClick'>;

export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props, ref) => {
export const SelectControl = React.forwardRef<HTMLButtonElement, ControlProps>((props, ref) => {
const {
toggleOpen,
clearValue,
Expand All @@ -57,8 +56,6 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,
value,
hasClear,
} = props;
const controlRef = React.useRef<HTMLElement>(null);
const handleControlRef = useForkRef<HTMLElement>(ref, controlRef);
const showOptionsText = Boolean(selectedOptionsContent);
const showPlaceholder = Boolean(placeholder && !showOptionsText);
const hasValue = Array.isArray(value) && !isEmpty(value.filter(Boolean));
Expand Down Expand Up @@ -118,7 +115,7 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,
{
onKeyDown,
onClick: toggleOpen,
ref: handleControlRef,
ref,
open: Boolean(open),
renderClear: (arg) => renderClearIcon(arg),
},
Expand All @@ -128,8 +125,9 @@ export const SelectControl = React.forwardRef<HTMLElement, ControlProps>((props,

return (
<React.Fragment>
<div className={selectControlBlock(controlMods)} ref={handleControlRef} role="group">
<div className={selectControlBlock(controlMods)} role="group">
<button
ref={ref}
className={selectControlButtonBlock(buttonMods, className)}
aria-haspopup="listbox"
aria-expanded={open}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const SelectPopup = React.forwardRef<HTMLDivElement, SelectPopupProps>(
</Sheet>
) : (
<Popup
className={b(null, className)}
contentClassName={b(null, className)}
qa={SelectQa.POPUP}
anchorRef={ref as React.RefObject<HTMLDivElement>}
placement={['bottom-start', 'bottom-end', 'top-start', 'top-end']}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export type SelectProps<T = any> = QAProps &
filterable?: boolean;
disablePortal?: boolean;
hasClear?: boolean;
onFocus?: (e: React.FocusEvent) => void;
onBlur?: (e: React.FocusEvent) => void;
children?:
| React.ReactElement<SelectOption<T>, typeof Option>
| React.ReactElement<SelectOption<T>, typeof Option>[]
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ export * from './utils/useListNavigation';
export * from './utils/useForkRef';
export * from './utils/setRef';
export {useOnFocusOutside} from './utils/useOnFocusOutside';
export * from './utils/interactions';
export * from './utils/xpath';
export * from './utils/useFileInput/useFileInput';
52 changes: 52 additions & 0 deletions src/components/utils/interactions/SyntheticFocusEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export class SyntheticFocusEvent<Target = Element> implements React.FocusEvent<Target> {
nativeEvent: FocusEvent;
target: EventTarget & Target;
currentTarget: EventTarget & Target;
relatedTarget: Element;
bubbles: boolean;
cancelable: boolean;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
timeStamp: number;
type: string;

constructor(
type: string,
nativeEvent: FocusEvent,
override: Partial<Pick<FocusEvent, 'target' | 'currentTarget'>> = {},
) {
this.nativeEvent = nativeEvent;
this.target = (override.target ?? nativeEvent.target) as EventTarget & Target;
this.currentTarget = (override.currentTarget ?? nativeEvent.currentTarget) as EventTarget &
Target;
this.relatedTarget = nativeEvent.relatedTarget as Element;
this.bubbles = nativeEvent.bubbles;
this.cancelable = nativeEvent.cancelable;
this.defaultPrevented = nativeEvent.defaultPrevented;
this.eventPhase = nativeEvent.eventPhase;
this.isTrusted = nativeEvent.isTrusted;
this.timeStamp = nativeEvent.timeStamp;
this.type = type;
}

isDefaultPrevented(): boolean {
return this.nativeEvent.defaultPrevented;
}

preventDefault(): void {
this.defaultPrevented = true;
this.nativeEvent.preventDefault();
}

stopPropagation(): void {
this.nativeEvent.stopPropagation();
this.isPropagationStopped = () => true;
}

isPropagationStopped(): boolean {
return false;
}

persist() {}
}
1 change: 1 addition & 0 deletions src/components/utils/interactions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {useFocusWithin} from './useFocusWithin';
Loading

0 comments on commit 5a3cf93

Please sign in to comment.