Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correcting focus behavior of react-search #28241

Merged
merged 17 commits into from
Jun 21, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export type SearchBoxSlots = {
};

// @public
export type SearchBoxState = ComponentState<SearchBoxSlots> & Required<Pick<InputState, 'size'>> & Required<Pick<SearchBoxProps, 'disabled'>>;
export type SearchBoxState = ComponentState<SearchBoxSlots> & Required<Pick<InputState, 'size'>> & Required<Pick<SearchBoxProps, 'disabled'>> & {
focused: boolean;
};

// @public
export const useSearchBox_unstable: (props: SearchBoxProps, ref: React_2.Ref<HTMLInputElement>) => SearchBoxState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ export type SearchBoxProps = ComponentProps<SearchBoxSlots>;
*/
export type SearchBoxState = ComponentState<SearchBoxSlots> &
Required<Pick<InputState, 'size'>> &
Required<Pick<SearchBoxProps, 'disabled'>>;
Required<Pick<SearchBoxProps, 'disabled'>> & {
focused: boolean;
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ exports[`SearchBox renders a default state 1`] = `
aria-label="clear"
class="fui-SearchBox__dismiss"
role="button"
tabindex="-1"
>
<svg
aria-hidden="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as React from 'react';
import { mergeCallbacks, resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities';
import { Input } from '@fluentui/react-input';
import {
mergeCallbacks,
resolveShorthand,
useControllableState,
useEventCallback,
useMergedRefs,
} from '@fluentui/react-utilities';
import { Input, InputState } from '@fluentui/react-input';
import type { SearchBoxProps, SearchBoxState } from './SearchBox.types';
import { DismissRegular, SearchRegular } from '@fluentui/react-icons';

Expand All @@ -14,14 +20,28 @@ import { DismissRegular, SearchRegular } from '@fluentui/react-icons';
* @param ref - reference to root HTMLElement of SearchBox
*/
export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref<HTMLInputElement>): SearchBoxState => {
const { size = 'medium', disabled = false, contentBefore, dismiss, contentAfter, ...inputProps } = props;
const { size = 'medium', disabled = false, root, contentBefore, dismiss, contentAfter, ...inputProps } = props;

const searchBoxRootRef = React.useRef<HTMLDivElement>(null);
const searchBoxRef = React.useRef<HTMLInputElement>(null);

const [value, setValue] = useControllableState({
state: props.value,
defaultState: props.defaultValue,
initialState: '',
});

// Tracks the focus of the component for the contentAfter and dismiss button
const [focused, setFocused] = React.useState(false);

const onFocus = useEventCallback(() => {
setFocused(true);
});

const onBlur: React.FocusEventHandler<HTMLSpanElement> = useEventCallback(ev => {
setFocused(!!searchBoxRootRef.current?.contains(ev.relatedTarget));
});

const state: SearchBoxState = {
components: {
root: Input,
Expand All @@ -30,9 +50,12 @@ export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref<HTML
},

root: {
ref,
ref: useMergedRefs(searchBoxRef, ref),
type: 'search',
input: {}, // defining here to have access in styles hook

disabled,
size,
value,

contentBefore: resolveShorthand(contentBefore, {
Expand All @@ -44,6 +67,10 @@ export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref<HTML

...inputProps,

root: resolveShorthand(root, {
required: true,
}),

onChange: useEventCallback(ev => {
const newValue = ev.target.value;
props.onChange?.(ev, { value: newValue });
Expand All @@ -55,15 +82,24 @@ export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref<HTML
children: <DismissRegular />,
role: 'button',
'aria-label': 'clear',
tabIndex: -1,
},
required: true,
}),
contentAfter: resolveShorthand(contentAfter, { required: true }),
contentAfter: resolveShorthand(contentAfter, {
required: true,
}),

disabled,
focused,
size,
};

const searchBoxRoot = state.root.root as InputState['root'];
searchBoxRoot.ref = useMergedRefs(searchBoxRoot.ref, searchBoxRootRef);
searchBoxRoot.onFocus = useEventCallback(mergeCallbacks(searchBoxRoot.onFocus, onFocus));
searchBoxRoot.onBlur = useEventCallback(mergeCallbacks(searchBoxRoot.onBlur, onBlur));

const onDismissClick = useEventCallback(mergeCallbacks(state.dismiss?.onClick, () => setValue('')));
if (state.dismiss) {
state.dismiss.onClick = onDismissClick;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ const useRootStyles = makeStyles({
paddingLeft: tokens.spacingHorizontalSNudge,
paddingRight: 0,

// dismiss + contentAfter appear on focus
'& + span': {
display: 'none',
},
'&:focus + span': {
display: 'flex',
},

// removes the WebKit pseudoelement styling
'::-webkit-search-decoration': {
display: 'none',
Expand All @@ -62,6 +54,11 @@ const useContentAfterStyles = makeStyles({
paddingLeft: tokens.spacingHorizontalM,
columnGap: tokens.spacingHorizontalXS,
},
rest: {
opacity: 0,
height: 0,
width: 0,
},
});

const useDismissClassName = makeResetStyles({
Expand Down Expand Up @@ -93,7 +90,7 @@ const useDismissStyles = makeStyles({
* Apply styling to the SearchBox slots based on the state
*/
export const useSearchBoxStyles_unstable = (state: SearchBoxState): SearchBoxState => {
const { disabled, size } = state;
const { disabled, focused, size } = state;

const rootStyles = useRootStyles();
const contentAfterStyles = useContentAfterStyles();
Expand All @@ -109,14 +106,18 @@ export const useSearchBoxStyles_unstable = (state: SearchBoxState): SearchBoxSta
dismissClassName,
disabled && dismissStyles.disabled,
dismissStyles[size],

state.dismiss.className,
);
}

if (state.contentAfter) {
state.contentAfter!.className = mergeClasses(
state.contentAfter.className = mergeClasses(
searchBoxClassNames.contentAfter,
contentAfterStyles.contentAfter,

!focused && contentAfterStyles.rest,

state.contentAfter.className,
);
} else if (state.dismiss) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,19 @@ import { SearchBox, SearchBoxProps } from '@fluentui/react-search';

import { FilterRegular } from '@fluentui/react-icons';

export const Default = (props: Partial<SearchBoxProps>) => <SearchBox {...props} contentAfter={<FilterRegular />} />;
// TODO: split into different stories
export const Default = (props: Partial<SearchBoxProps>) => (
<>
<SearchBox {...props} contentAfter={<FilterRegular />} size="small" placeholder="small" />
<SearchBox {...props} contentAfter={<FilterRegular />} size="medium" placeholder="medium" />
<SearchBox {...props} contentAfter={<FilterRegular />} size="large" placeholder="large" />
<SearchBox {...props} contentAfter={null} placeholder="no contentAfter" />
<SearchBox {...props} contentAfter={<FilterRegular />} disabled placeholder="disabled" />
<SearchBox
{...props}
contentAfter={<FilterRegular tabIndex={0} onClick={() => console.log('clicked')} />}
placeholder="contentAfter button"
/>
<SearchBox root={{ onFocus: () => console.log('test') }} placeholder="custom onFocus" />
</>
);