Skip to content

Commit b4c99bb

Browse files
authored
New Source viewer gets search modifier support (#8060)
* Add search modifier support for new Source viewer * Update screenshots * Fix prettier.config.js
1 parent 4c41685 commit b4c99bb

14 files changed

+291
-121
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,48 @@
11
.Container {
2-
display: flex;
32
width: 100%;
3+
height: 2.5em;
4+
border: 2px solid transparent;
5+
border-top-color: var(--color-contrast);
6+
display: flex;
7+
}
8+
.Container:focus-within {
9+
border-color: var(--color-brand);
10+
}
11+
12+
.InputAndResults {
13+
flex: 1 1 auto;
14+
padding: 0 0.5em;
15+
display: flex;
416
align-items: center;
5-
padding: 0.5em;
617
background-color: var(--background-color-contrast-2);
7-
border-top: 1px solid var(--color-contrast);
818
}
919

10-
.Icon {
20+
.SearchIcon {
1121
width: 2em;
1222
height: 2em;
1323
padding: 0.25em;
1424
flex: 0 0 2em;
1525
display: inline-block;
26+
color: var(--color-dim);
1627
}
1728

1829
.Input {
1930
width: 100%;
2031
height: 2em;
2132
padding: 0 0.5ch;
2233
background: none;
23-
border: 2px solid transparent;
34+
border: none;
2435
outline: none;
25-
background-color: var(--background-color-default);
36+
background-color: var(--background-color-contrast-2);
2637
color: var(--color-default);
2738

2839
font-family: var(--font-family-monospace);
2940
font-size: var(--font-size-regular);
3041
}
3142
.Input:focus {
32-
background-color: var(--background-color-contrast-1);
33-
border-color: var(--color-brand);
43+
outline: none;
44+
border: none;
45+
box-shadow: none;
3446
}
3547

3648
.Results {
@@ -42,20 +54,52 @@
4254
margin-left: 0.5ch;
4355
padding: 0 0.5ch;
4456
font-size: var(--font-size-small);
57+
color: var(--color-dim);
4558
}
4659

4760
.ResultsIconButton {
61+
font-family: var(--font-family-monospace);
62+
font-size: var(--font-size-regular);
4863
display: flex;
64+
align-items: center;
65+
justify-content: center;
4966
background: none;
5067
border: none;
51-
color: var(--color-default);
68+
color: var(--color-dim);
5269
padding: 0;
53-
width: 1em;
54-
height: 1em;
70+
width: 1.5em;
71+
height: 1.5em;
72+
border-radius: 0.25em;
73+
}
74+
.ResultsIconButton:hover:not(.ResultsIconButton:disabled):not(.ResultsIconButtonActive) {
75+
color: var(--color-default);
76+
}
77+
.ResultsIconButton:disabled {
78+
color: var(--color-dimmer);
79+
}
80+
.ResultsIconButtonActive {
81+
color: var(--background-color-primary-button);
5582
}
5683

5784
.ResultsIcon {
58-
width: 1em;
59-
height: 1em;
85+
width: 1.5em;
86+
height: 1.5em;
6087
display: inline-block;
88+
color: currentColor;
89+
}
90+
91+
.Modifiers {
92+
flex: 0 0 auto;
93+
padding: 0 1ch;
94+
gap: 1ch;
95+
display: flex;
96+
align-items: center;
97+
background-color: var(--background-color-contrast-2);
98+
border-left: 1px solid var(--background-color-default);
99+
color: var(--color-dim);
100+
user-select: none;
101+
}
102+
103+
.WholeWordText {
104+
text-decoration: underline;
61105
}

packages/bvaughn-architecture-demo/components/sources/SourceSearch.tsx

+61-28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export default function SourceSearch({
1313
}) {
1414
const [searchState, searchActions] = useContext(SourceSearchContext);
1515

16+
const { caseSensitive, regex, wholeWord } = searchState.modifiers;
17+
1618
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
1719
searchActions.search(event.currentTarget.value);
1820
};
@@ -50,20 +52,79 @@ export default function SourceSearch({
5052
results = (
5153
<div className={styles.Results} data-test-id="SearchResultsLabel">
5254
{searchState.index + 1} of {searchState.results.length} results
55+
</div>
56+
);
57+
} else if (searchState.query !== "") {
58+
results = <div className={styles.Results}>No results found</div>;
59+
}
60+
61+
return (
62+
<div className={styles.Container} data-test-id="SourceSearch">
63+
<div className={styles.InputAndResults}>
64+
<Icon className={styles.SearchIcon} type="search" />
65+
<input
66+
autoFocus
67+
className={styles.Input}
68+
data-test-id="SourceSearchInput"
69+
onChange={onChange}
70+
onKeyDown={onKeyDown}
71+
placeholder="Find"
72+
ref={inputRef}
73+
type="text"
74+
value={searchState.query}
75+
/>
76+
{results}
5377
<button
5478
className={styles.ResultsIconButton}
5579
data-test-id="SourceSearchGoToPreviousButton"
80+
disabled={searchState.results.length === 0}
5681
onClick={searchActions.goToPrevious}
5782
>
5883
<Icon className={styles.ResultsIcon} type="up" />
5984
</button>
6085
<button
6186
className={styles.ResultsIconButton}
6287
data-test-id="SourceSearchGoToNextButton"
88+
disabled={searchState.results.length === 0}
6389
onClick={searchActions.goToNext}
6490
>
6591
<Icon className={styles.ResultsIcon} type="down" />
6692
</button>
93+
</div>
94+
<div className={styles.Modifiers}>
95+
Modifiers:
96+
<button
97+
className={[styles.ResultsIconButton, regex && styles.ResultsIconButtonActive].join(" ")}
98+
data-test-id="SearchRegExButton"
99+
onClick={() => searchActions.setModifiers({ ...searchState.modifiers, regex: !regex })}
100+
>
101+
.*
102+
</button>
103+
<button
104+
className={[
105+
styles.ResultsIconButton,
106+
caseSensitive && styles.ResultsIconButtonActive,
107+
].join(" ")}
108+
data-test-id="SearchCaseSensitivityButton"
109+
onClick={() =>
110+
searchActions.setModifiers({ ...searchState.modifiers, caseSensitive: !caseSensitive })
111+
}
112+
>
113+
Aa
114+
</button>
115+
<button
116+
className={[styles.ResultsIconButton, wholeWord && styles.ResultsIconButtonActive].join(
117+
" "
118+
)}
119+
data-test-id="SearchWholeWordButtonButton"
120+
onClick={() =>
121+
searchActions.setModifiers({ ...searchState.modifiers, wholeWord: !wholeWord })
122+
}
123+
>
124+
<span className={styles.WholeWordText}>ab</span>
125+
</button>
126+
</div>
127+
<div className={styles.Modifiers}>
67128
<button
68129
className={styles.ResultsIconButton}
69130
data-test-id="SourceSearchClearButton"
@@ -72,34 +133,6 @@ export default function SourceSearch({
72133
<Icon className={styles.ResultsIcon} type="cancel" />
73134
</button>
74135
</div>
75-
);
76-
} else if (searchState.query !== "") {
77-
results = (
78-
<div className={styles.Results}>
79-
No results found
80-
<button className={styles.ResultsIconButton} onClick={searchActions.disable}>
81-
<Icon className={styles.ResultsIcon} type="cancel" />
82-
</button>
83-
</div>
84-
);
85-
}
86-
87-
return (
88-
<div className={styles.Container} data-test-id="SourceSearch">
89-
<Icon className={styles.Icon} type="search" />
90-
<input
91-
autoFocus
92-
className={styles.Input}
93-
data-test-id="SourceSearchInput"
94-
onChange={onChange}
95-
onKeyDown={onKeyDown}
96-
placeholder="Find"
97-
ref={inputRef}
98-
type="text"
99-
value={searchState.query}
100-
/>
101-
102-
{results}
103136
</div>
104137
);
105138
}

packages/bvaughn-architecture-demo/components/sources/SourceSearchContext.tsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,23 @@ import { ReplayClientContext } from "shared/client/ReplayClientContext";
66

77
import useSourceSearch, { Actions, SetScope, State } from "./hooks/useSourceSearch";
88

9-
export const SourceSearchContext = createContext<[State, Actions]>(null as any);
9+
export type SearchModifiers = {
10+
caseSensitive: boolean;
11+
regex: boolean;
12+
wholeWord: boolean;
13+
};
14+
15+
export type SearchContextType = [State, Actions];
16+
17+
export const SourceSearchContext = createContext<SearchContextType>(null as any);
1018

1119
export function SourceSearchContextRoot({ children }: { children: ReactNode }) {
1220
const client = useContext(ReplayClientContext);
1321
const { focusedSourceId } = useContext(SourcesContext);
1422

15-
const [state, actions] = useSourceSearch();
23+
const [state, dispatch] = useSourceSearch();
1624

17-
const context = useMemo<[State, Actions]>(() => [state, actions], [state, actions]);
25+
const context = useMemo<SearchContextType>(() => [state, dispatch], [dispatch, state]);
1826

1927
// Keep source search state in sync with the focused source.
2028
useEffect(() => {
@@ -27,8 +35,8 @@ export function SourceSearchContextRoot({ children }: { children: ReactNode }) {
2735
}
2836
}
2937

30-
updateSourceContents(focusedSourceId, actions.setScope);
31-
}, [client, actions, focusedSourceId]);
38+
updateSourceContents(focusedSourceId, dispatch.setScope);
39+
}, [client, dispatch.setScope, focusedSourceId]);
3240

3341
return <SourceSearchContext.Provider value={context}>{children}</SourceSearchContext.Provider>;
3442
}

packages/bvaughn-architecture-demo/components/sources/hooks/useSearch.test.tsx

+31-12
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@ import useSearch, { Actions, ScopeId, SearchFunction, State } from "./useSearch"
55

66
type Item = string;
77
type Result = string;
8+
type QueryData = boolean;
89

910
const DEFAULT_ITEMS: Item[] = ["foo", "bar", "baz"];
1011
const DEFAULT_SCOPE = "default";
1112

12-
function stableSearch(query: string, items: Item[]) {
13-
return items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
13+
function stableSearch(query: string, items: Item[], caseSensitive: QueryData | null = false) {
14+
const needle = caseSensitive ? query : query.toLowerCase();
15+
return items.filter(item => {
16+
if (!caseSensitive) {
17+
item.toLowerCase();
18+
}
19+
return item.includes(needle);
20+
});
1421
}
1522

1623
describe("useSearch", () => {
17-
let currentActions: Actions | null = null;
18-
let currentState: State<Item, Result> | null = null;
24+
let currentActions: Actions<QueryData> | null = null;
25+
let currentState: State<Item, Result, QueryData> | null = null;
1926
let renderResult: RenderResult | null = null;
20-
let stableSearchMock: jest.MockedFunction<SearchFunction<Item, Result>> = null as any;
27+
let stableSearchMock: jest.MockedFunction<SearchFunction<Item, Result, QueryData>> = null as any;
2128

2229
beforeEach(() => {
2330
stableSearchMock = jest.fn().mockImplementation(stableSearch);
@@ -28,7 +35,7 @@ describe("useSearch", () => {
2835
});
2936

3037
function Component({ items, scopeId }: { items: Item[]; scopeId: ScopeId }) {
31-
const [state, actions] = useSearch<Item, Result>(items, stableSearchMock, scopeId);
38+
const [state, actions] = useSearch<Item, Result, QueryData>(items, stableSearchMock, scopeId);
3239

3340
useEffect(() => {
3441
currentActions = actions;
@@ -66,21 +73,27 @@ describe("useSearch", () => {
6673
});
6774
}
6875

69-
function search(query: string) {
76+
function search(query: string, queryData: QueryData = false) {
7077
act(() => {
71-
currentActions?.search(query);
78+
currentActions?.search(query, queryData);
7279
});
7380
}
7481

75-
it("should refine search results when query text or items change", async () => {
82+
it("should refine search results when query text, items, or query data change", async () => {
7683
render(DEFAULT_ITEMS);
7784
search("b");
7885
expect(currentState?.results).toEqual(["bar", "baz"]);
7986

80-
search("ba");
87+
search("bA");
8188
expect(currentState?.results).toEqual(["bar", "baz"]);
8289

83-
search("bar");
90+
search("bAr");
91+
expect(currentState?.results).toEqual(["bar"]);
92+
93+
search("bAr", true);
94+
expect(currentState?.results).toEqual([]);
95+
96+
search("bAr", false);
8497
expect(currentState?.results).toEqual(["bar"]);
8598

8699
search("q");
@@ -214,7 +227,7 @@ describe("useSearch", () => {
214227
expect(currentState?.results).toEqual(["bar", "baz"]);
215228
});
216229

217-
it("should (only) re-run search when query text changes", async () => {
230+
it("should (only) re-run search when query or queryData changes", async () => {
218231
render(DEFAULT_ITEMS);
219232
expect(stableSearchMock).toHaveBeenCalledTimes(0);
220233

@@ -226,6 +239,12 @@ describe("useSearch", () => {
226239

227240
search("ba");
228241
expect(stableSearchMock).toHaveBeenCalledTimes(2);
242+
243+
search("ba", true);
244+
expect(stableSearchMock).toHaveBeenCalledTimes(3);
245+
246+
render(DEFAULT_ITEMS);
247+
expect(stableSearchMock).toHaveBeenCalledTimes(3);
229248
});
230249
});
231250
});

0 commit comments

Comments
 (0)