Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 121 additions & 96 deletions src/components/shared/IdleSearchField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { PureComponent } from 'react';

import React, {
forwardRef,
useEffect,
useRef,
useState,
} from 'react';

import classNames from 'classnames';
import { Localized } from '@fluent/react';

Expand All @@ -17,133 +24,151 @@
readonly title: string | null;
};

type State = {
value: string;
previousDefaultValue: string;
};
export const IdleSearchField = forwardRef<HTMLInputElement, Props>(
(
{
onIdleAfterChange,
onFocus,
onBlur,
idlePeriod,
defaultValue,
className,
title,
},
forwardedRef
) => {
const [value, setValue] = useState(defaultValue || '');

const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const previouslyNotifiedValue = useRef(value);

const internalInputRef = useRef<HTMLInputElement | null>(null);

// Sync forwarded ref with internal input ref
const setRefs = (input: HTMLInputElement | null) => {
internalInputRef.current = input;

if (typeof forwardedRef === 'function') {
forwardedRef(input);
} else if (forwardedRef) {
forwardedRef.current = input;
}
};

export class IdleSearchField extends PureComponent<Props, State> {
_timeout: NodeJS.Timeout | null = null;
_previouslyNotifiedValue: string;
_input: HTMLInputElement | null = null;
_takeInputRef = (input: HTMLInputElement | null) => (this._input = input);

constructor(props: Props) {
super(props);
this.state = {
value: props.defaultValue || '',
previousDefaultValue: props.defaultValue || '',
// Sync state when defaultValue changes externally
useEffect(() => {
setValue(defaultValue || '');
previouslyNotifiedValue.current = defaultValue || '';
}, [defaultValue]);

// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

const notifyIfChanged = (newValue: string) => {
if (newValue !== previouslyNotifiedValue.current) {
previouslyNotifiedValue.current = newValue;
onIdleAfterChange(newValue);
}
};
this._previouslyNotifiedValue = this.state.value;
}

_onSearchFieldFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget.select();

if (this.props.onFocus) {
this.props.onFocus();
}
};

_onSearchFieldBlur = (e: { relatedTarget: Element | null }) => {
if (this.props.onBlur) {
this.props.onBlur(e.relatedTarget);
}
};

_onSearchFieldChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
value: e.currentTarget.value,
});

if (this._timeout) {
clearTimeout(this._timeout);
}
this._timeout = setTimeout(this._onTimeout, this.props.idlePeriod);
};

_onTimeout = () => {
this._timeout = null;
this._notifyIfChanged(this.state.value);
};

_notifyIfChanged(value: string) {
if (value !== this._previouslyNotifiedValue) {
this._previouslyNotifiedValue = value;
this.props.onIdleAfterChange(value);
}
}
const onTimeout = () => {
timeoutRef.current = null;
notifyIfChanged(value);
};

_onClearButtonClick = () => {
if (this._input) {
this._input.focus();
}
const onSearchFieldFocus = (
e: React.FocusEvent<HTMLInputElement>
) => {
e.currentTarget.select();

if (this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
if (onFocus) {
onFocus();
}
};

this.setState({ value: '' });
this._notifyIfChanged('');
};
const onSearchFieldBlur = (
e: React.FocusEvent<HTMLInputElement>
) => {
if (onBlur) {
onBlur(e.relatedTarget);
}
};

_onFormSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
const onSearchFieldChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const newValue = e.currentTarget.value;

override componentDidUpdate(prevProps: Props) {
// When the defaultValue prop changes externally (e.g., from Redux),
// getDerivedStateFromProps will update the state value. We need to sync
// _previouslyNotifiedValue to match so that subsequent changes are detected
// correctly by _notifyIfChanged.
if (prevProps.defaultValue !== this.props.defaultValue) {
this._previouslyNotifiedValue = this.state.value;
}
}
setValue(newValue);

static getDerivedStateFromProps(props: Props, state: State) {
if (props.defaultValue !== state.previousDefaultValue) {
return {
previousDefaultValue: props.defaultValue || '',
value: props.defaultValue || '',
};
}
return null;
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(onTimeout, idlePeriod);
};

const onClearButtonClick = () => {
if (internalInputRef.current) {
internalInputRef.current.focus();
}

if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}

setValue('');
notifyIfChanged('');
};

const onFormSubmit = (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault();
};

override render() {
const { className, title } = this.props;
return (
<form
className={classNames('idleSearchField', className)}
onSubmit={this._onFormSubmit}
onSubmit={onFormSubmit}
>
<Localized
id="IdleSearchField--search-input"
attrs={{ placeholder: true }}
>
<input
ref={setRefs}
type="search"
name="search"
placeholder="Enter filter terms"
className="idleSearchFieldInput photon-input"
required={true}
title={title ?? undefined}
value={this.state.value}
onChange={this._onSearchFieldChange}
onFocus={this._onSearchFieldFocus}
onBlur={this._onSearchFieldBlur}
ref={this._takeInputRef}
value={value}
onChange={onSearchFieldChange}
onFocus={onSearchFieldFocus}
onBlur={onSearchFieldBlur}
/>
</Localized>

<input
type="reset"
className="idleSearchFieldButton"
onClick={this._onClearButtonClick}
onClick={onClearButtonClick}
tabIndex={-1}
/>
</form>
);
}
}
);

IdleSearchField.displayName = 'IdleSearchField';
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure yarn test-all passes before submitting a PR, and also do some manual testing to verify that the change works as expected. This looks like a markdown code block ending in a TS file.

Loading