Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: prevent workspace search bar text from getting garbled#9703

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Parkreiner merged 11 commits intomainfromfilter-fix
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
11 commits
Select commitHold shift + click to select a range
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
68 changes: 43 additions & 25 deletionssite/src/components/Filter/filter.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,11 +20,11 @@ import {
} from "api/errors";
import { useFilterMenu } from "./menu";
import { BaseOption } from "./options";
import debounce from "just-debounce-it";
import MenuList from "@mui/material/MenuList";
import { Loader } from "components/Loader/Loader";
import Divider from "@mui/material/Divider";
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
import { useDebouncedFunction } from "hooks/debounce";

export type PresetFilter = {
name: string;
Expand DownExpand Up@@ -58,7 +58,7 @@ export const useFilter = ({
}
};

const debounceUpdate = debounce(
const{ debounced:debounceUpdate, cancelDebounce } = useDebouncedFunction(
(values: string | FilterValues) => update(values),
500,
);
Expand All@@ -69,6 +69,7 @@ export const useFilter = ({
query,
update,
debounceUpdate,
cancelDebounce,
values,
used,
};
Expand DownExpand Up@@ -130,17 +131,7 @@ export const MenuSkeleton = () => (
<BaseSkeleton sx={{ minWidth: 200, flexShrink: 0 }} />
);

export const Filter = ({
filter,
isLoading,
error,
skeleton,
options,
learnMoreLink,
learnMoreLabel2,
learnMoreLink2,
presets,
}: {
type FilterProps = {
filter: ReturnType<typeof useFilter>;
skeleton: ReactNode;
isLoading: boolean;
Expand All@@ -150,20 +141,42 @@ export const Filter = ({
error?: unknown;
options?: ReactNode;
presets: PresetFilter[];
}) => {
const shouldDisplayError = hasError(error) && isApiValidationError(error);
const hasFilterQuery = filter.query !== "";
const [searchQuery, setSearchQuery] = useState(filter.query);
const inputRef = useRef<HTMLInputElement>(null);
};

export const Filter = ({
filter,
isLoading,
error,
skeleton,
options,
learnMoreLink,
learnMoreLabel2,
learnMoreLink2,
presets,
}: FilterProps) => {
// Storing local copy of the filter query so that it can be updated more
// aggressively without re-renders rippling out to the rest of the app every
// single time. Exists for performance reasons - not really a good way to
// remove this; render keys would cause the component to remount too often
const [queryCopy, setQueryCopy] = useState(filter.query);
const textboxInputRef = useRef<HTMLInputElement>(null);

// Conditionally re-syncs the parent and local filter queries
useEffect(() => {
// We don't want to update this while the user is typing something or has the focus in the input
const isFocused = document.activeElement === inputRef.current;
if (!isFocused) {
setSearchQuery(filter.query);
const hasSelfOrInnerFocus =
textboxInputRef.current?.contains(document.activeElement) ?? false;

// This doesn't address all state sync issues - namely, what happens if the
// user removes focus just after this synchronizing effect fires. Also need
// to rely on onBlur behavior as an extra safety measure
if (!hasSelfOrInnerFocus) {
setQueryCopy(filter.query);
}
}, [filter.query]);

const shouldDisplayError = hasError(error) && isApiValidationError(error);
const hasFilterQuery = filter.query !== "";

return (
<Box
sx={{
Expand DownExpand Up@@ -198,12 +211,17 @@ export const Filter = ({
"aria-label": "Filter",
name: "query",
placeholder: "Search...",
value:searchQuery,
ref:inputRef,
value:queryCopy,
ref:textboxInputRef,
onChange: (e) => {
setSearchQuery(e.target.value);
setQueryCopy(e.target.value);
filter.debounceUpdate(e.target.value);
},
onBlur: () => {
if (queryCopy !== filter.query) {
setQueryCopy(filter.query);
}
},
sx: {
borderRadius: "6px",
borderTopLeftRadius: 0,
Expand Down
193 changes: 193 additions & 0 deletionssite/src/hooks/debounce.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
import{renderHook}from"@testing-library/react";
import{useDebouncedFunction,useDebouncedValue}from"./debounce";

beforeAll(()=>{
jest.useFakeTimers();
jest.spyOn(global,"setTimeout");
});

afterAll(()=>{
jest.useRealTimers();
jest.clearAllMocks();
});

// Most UI tests should be structure from the user's experience, but just
// because these are more abstract, general-purpose hooks, it seemed harder to
// do that. Had to bring in some mocks
functionrenderDebouncedValue<T=unknown>(value:T,time:number){
returnrenderHook(
({ value, time}:{value:T;time:number})=>{
returnuseDebouncedValue(value,time);
},
{
initialProps:{ value, time},
},
);
}

functionrenderDebouncedFunction<Argsextendsunknown[]>(
callbackArg:(...args:Args)=>void|Promise<void>,
time:number,
){
returnrenderHook(
({ callback, time}:{callback:typeofcallbackArg;time:number})=>{
returnuseDebouncedFunction<Args>(callback,time);
},
{
initialProps:{callback:callbackArg, time},
},
);
}

describe(`${useDebouncedValue.name}`,()=>{
it("Should immediately return out the exact same value (by reference) on mount",()=>{
constvalue={};
const{ result}=renderDebouncedValue(value,2000);

expect(result.current).toBe(value);
expect.hasAssertions();
});

it("Should not immediately resync state as the hook re-renders with new value argument",async()=>{
letvalue=0;
consttime=5000;

const{ result, rerender}=renderDebouncedValue(value,time);
expect(result.current).toEqual(0);

for(leti=1;i<=5;i++){
setTimeout(()=>{
value++;
rerender({ value, time});
},i*100);
}

awaitjest.advanceTimersByTimeAsync(time-100);
expect(result.current).toEqual(0);
expect.hasAssertions();
});

it("Should resync after specified milliseconds pass with no change to arguments",async()=>{
constinitialValue=false;
consttime=5000;

const{ result, rerender}=renderDebouncedValue(initialValue,time);
expect(result.current).toEqual(false);

rerender({value:!initialValue, time});
awaitjest.runAllTimersAsync();

expect(result.current).toEqual(true);
expect.hasAssertions();
});
});

describe(`${useDebouncedFunction.name}`,()=>{
describe("hook",()=>{
it("Should provide stable function references across re-renders",()=>{
consttime=5000;
const{ result, rerender}=renderDebouncedFunction(jest.fn(),time);

const{debounced:oldDebounced,cancelDebounce:oldCancel}=
result.current;

rerender({callback:jest.fn(), time});
const{debounced:newDebounced,cancelDebounce:newCancel}=
result.current;

expect(oldDebounced).toBe(newDebounced);
expect(oldCancel).toBe(newCancel);
expect.hasAssertions();
});

it("Resets any pending debounces if the timer argument changes",async()=>{
consttime=5000;
letcount=0;
constincrementCount=()=>{
count++;
};

const{ result, rerender}=renderDebouncedFunction(
incrementCount,
time,
);

result.current.debounced();
rerender({callback:incrementCount,time:time+1});

awaitjest.runAllTimersAsync();
expect(count).toEqual(0);
expect.hasAssertions();
});
});

describe("debounced function",()=>{
it("Resolve the debounce after specified milliseconds pass with no other calls",async()=>{
letvalue=false;
const{ result}=renderDebouncedFunction(()=>{
value=!value;
},100);

result.current.debounced();

awaitjest.runOnlyPendingTimersAsync();
expect(value).toBe(true);
expect.hasAssertions();
});

it("Always uses the most recent callback argument passed in (even if it switches while a debounce is queued)",async()=>{
letcount=0;
consttime=500;

const{ result, rerender}=renderDebouncedFunction(()=>{
count=1;
},time);

result.current.debounced();
rerender({
callback:()=>{
count=9999;
},
time,
});

awaitjest.runAllTimersAsync();
expect(count).toEqual(9999);
expect.hasAssertions();
});

it("Should reset the debounce timer with repeated calls to the method",async()=>{
letcount=0;
const{ result}=renderDebouncedFunction(()=>{
count++;
},2000);

for(leti=0;i<10;i++){
setTimeout(()=>{
result.current.debounced();
},i*100);
}

awaitjest.runAllTimersAsync();
expect(count).toBe(1);
expect.hasAssertions();
});
});

describe("cancelDebounce function",()=>{
it("Should be able to cancel a pending debounce",async()=>{
letcount=0;
const{ result}=renderDebouncedFunction(()=>{
count++;
},2000);

const{ debounced, cancelDebounce}=result.current;
debounced();
cancelDebounce();

awaitjest.runAllTimersAsync();
expect(count).toEqual(0);
expect.hasAssertions();
});
});
});
Loading

[8]ページ先頭

©2009-2025 Movatter.jp