Skip to content

Commit 3664759

Browse files
(Lookup): (scope) mitigate re-rendering by capsulating state changes
1 parent 4ef6024 commit 3664759

File tree

1 file changed

+141
-153
lines changed

1 file changed

+141
-153
lines changed

src/scripts/Lookup.tsx

Lines changed: 141 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,11 @@ const LookupSelectedState: FC<LookupSelectedStateProps> = ({
187187
type LookupScopeSelectorProps = {
188188
scopes: LookupScope[];
189189
targetScope?: string;
190-
scopeOpened: boolean;
191-
scopeFocusedIndex: number;
192190
disabled?: boolean;
193191
scopeListboxId: string;
194192
getScopeOptionId: (index: number) => string;
195-
onScopeMenuClick: () => void;
196-
onScopeBlur: (e: FocusEvent) => void;
197-
onScopeKeyDown: (e: KeyboardEvent) => void;
198-
onScopeSelect: (scope: string) => void;
193+
onScopeMenuClick?: () => void;
194+
onScopeSelect?: (scope: string) => void;
199195
};
200196

201197
/**
@@ -204,18 +200,146 @@ type LookupScopeSelectorProps = {
204200
const LookupScopeSelector: FC<LookupScopeSelectorProps> = ({
205201
scopes,
206202
targetScope,
207-
scopeOpened,
208-
scopeFocusedIndex,
209203
disabled,
210204
scopeListboxId,
211205
getScopeOptionId,
212-
onScopeMenuClick,
213-
onScopeBlur,
214-
onScopeKeyDown,
215-
onScopeSelect,
206+
onScopeMenuClick: onScopeMenuClick_,
207+
onScopeSelect: onScopeSelect_,
216208
}) => {
209+
const [scopeOpened, setScopeOpened] = useState(false);
210+
const [scopeFocusedIndex, setScopeFocusedIndex] = useState<number>(-1);
211+
217212
const currentScope = scopes.find((scope) => scope.label === targetScope);
218213

214+
// Scroll focused scope element into view
215+
const scrollFocusedScopeIntoView = useEventCallback(
216+
(nextFocusedIndex: number) => {
217+
if (nextFocusedIndex < 0 || !scopes) {
218+
return;
219+
}
220+
221+
const scopeDropdown = document.getElementById(scopeListboxId);
222+
if (!scopeDropdown) {
223+
return;
224+
}
225+
226+
const targetElement = scopeDropdown.querySelector(
227+
`#${CSS.escape(getScopeOptionId(nextFocusedIndex))}`
228+
);
229+
230+
if (!(targetElement instanceof HTMLElement)) {
231+
return;
232+
}
233+
234+
targetElement.focus();
235+
}
236+
);
237+
238+
const onScopeSelect = useEventCallback((scope: string) => {
239+
setScopeOpened(false);
240+
onScopeSelect_?.(scope);
241+
});
242+
243+
const onScopeMenuClick = useEventCallback(() => {
244+
setScopeOpened(!scopeOpened);
245+
onScopeMenuClick_?.();
246+
});
247+
248+
const onScopeKeyDown = useKeyHandler({
249+
type: 'scope',
250+
opened: scopeOpened,
251+
onOpen: () => {
252+
if (!scopes) return;
253+
setScopeOpened(true);
254+
setScopeFocusedIndex(0);
255+
},
256+
onClose: () => {
257+
setScopeOpened(false);
258+
setScopeFocusedIndex(-1);
259+
},
260+
onNavigateDown: () => {
261+
if (!scopes) return;
262+
const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1);
263+
setScopeFocusedIndex(nextIndex);
264+
scrollFocusedScopeIntoView(nextIndex);
265+
},
266+
onNavigateUp: () => {
267+
if (!scopes) return;
268+
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
269+
setScopeFocusedIndex(prevIndex);
270+
scrollFocusedScopeIntoView(prevIndex);
271+
},
272+
onSelect: () => {
273+
if (!scopes) return;
274+
if (scopeOpened && scopeFocusedIndex >= 0) {
275+
const selectedScope = scopes[scopeFocusedIndex];
276+
if (selectedScope) {
277+
onScopeSelect(selectedScope.label);
278+
setScopeOpened(false);
279+
setScopeFocusedIndex(-1);
280+
}
281+
} else {
282+
setScopeOpened(!scopeOpened);
283+
}
284+
},
285+
isTabNavigationIgnored: (direction) => {
286+
if (!scopes) {
287+
return false;
288+
}
289+
290+
return (
291+
scopeFocusedIndex === -1 ||
292+
(direction === 'backward' && scopeFocusedIndex <= 0) ||
293+
(direction === 'forward' && scopeFocusedIndex >= scopes.length - 1)
294+
);
295+
},
296+
onTabNavigation: (direction) => {
297+
if (!scopes) return;
298+
if (direction === 'backward') {
299+
if (scopeFocusedIndex <= 0) {
300+
setScopeOpened(false);
301+
setScopeFocusedIndex(-1);
302+
} else {
303+
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
304+
setScopeFocusedIndex(prevIndex);
305+
scrollFocusedScopeIntoView(prevIndex);
306+
}
307+
} else {
308+
if (scopeFocusedIndex >= scopes.length - 1) {
309+
setScopeOpened(false);
310+
setScopeFocusedIndex(-1);
311+
} else {
312+
const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1);
313+
setScopeFocusedIndex(nextIndex);
314+
scrollFocusedScopeIntoView(nextIndex);
315+
}
316+
}
317+
},
318+
});
319+
320+
const onScopeBlur = useEventCallback((e: FocusEvent) => {
321+
if (e.relatedTarget !== null) {
322+
if (!scopes) {
323+
return;
324+
}
325+
326+
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
327+
const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1);
328+
329+
if (
330+
e.relatedTarget.id === getScopeOptionId(prevIndex) ||
331+
e.relatedTarget.id === getScopeOptionId(nextIndex)
332+
) {
333+
// catch keyborad event
334+
return;
335+
}
336+
}
337+
338+
setTimeout(() => {
339+
setScopeOpened(false);
340+
}, 10);
341+
});
342+
219343
return (
220344
<div className='slds-combobox_object-switcher slds-combobox-addon_start'>
221345
<div className='slds-form-element'>
@@ -781,8 +905,6 @@ export const Lookup = createFC<LookupProps, { isFormElement: boolean }>(
781905
(scopes && scopes.length > 0 ? scopes[0].label : undefined)
782906
);
783907
const [focusedValue, setFocusedValue] = useState<string | undefined>();
784-
const [scopeOpened, setScopeOpened] = useState(false);
785-
const [scopeFocusedIndex, setScopeFocusedIndex] = useState<number>(-1);
786908

787909
const listHeaderIdSeed = useMemo(
788910
() => [...data.map((entry) => entry.value), 'header'].join('-'),
@@ -864,30 +986,6 @@ export const Lookup = createFC<LookupProps, { isFormElement: boolean }>(
864986
}
865987
);
866988

867-
// Scroll focused scope element into view
868-
const scrollFocusedScopeIntoView = useEventCallback(
869-
(nextFocusedIndex: number) => {
870-
if (nextFocusedIndex < 0 || !scopes) {
871-
return;
872-
}
873-
874-
const scopeDropdown = document.getElementById(scopeListboxId);
875-
if (!scopeDropdown) {
876-
return;
877-
}
878-
879-
const targetElement = scopeDropdown.querySelector(
880-
`#${CSS.escape(getScopeOptionId(nextFocusedIndex))}`
881-
);
882-
883-
if (!(targetElement instanceof HTMLElement)) {
884-
return;
885-
}
886-
887-
targetElement.focus();
888-
}
889-
);
890-
891989
// Set initial focus when dropdown opens
892990
useEffect(() => {
893991
if (!opened) {
@@ -1055,115 +1153,6 @@ export const Lookup = createFC<LookupProps, { isFormElement: boolean }>(
10551153
}, 10);
10561154
});
10571155

1058-
const onScopeSelect = useEventCallback((scope: string) => {
1059-
setTargetScope(scope);
1060-
setScopeOpened(false);
1061-
onScopeSelect_?.(scope);
1062-
});
1063-
1064-
const onScopeMenuClick = useEventCallback(() => {
1065-
setScopeOpened(!scopeOpened);
1066-
onScopeMenuClick_?.();
1067-
});
1068-
1069-
const onScopeKeyDown = useKeyHandler({
1070-
type: 'scope',
1071-
opened: scopeOpened,
1072-
onOpen: () => {
1073-
if (!scopes) return;
1074-
setScopeOpened(true);
1075-
setScopeFocusedIndex(0);
1076-
},
1077-
onClose: () => {
1078-
setScopeOpened(false);
1079-
setScopeFocusedIndex(-1);
1080-
},
1081-
onNavigateDown: () => {
1082-
if (!scopes) return;
1083-
const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1);
1084-
setScopeFocusedIndex(nextIndex);
1085-
scrollFocusedScopeIntoView(nextIndex);
1086-
},
1087-
onNavigateUp: () => {
1088-
if (!scopes) return;
1089-
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
1090-
setScopeFocusedIndex(prevIndex);
1091-
scrollFocusedScopeIntoView(prevIndex);
1092-
},
1093-
onSelect: () => {
1094-
if (!scopes) return;
1095-
if (scopeOpened && scopeFocusedIndex >= 0) {
1096-
const selectedScope = scopes[scopeFocusedIndex];
1097-
if (selectedScope) {
1098-
onScopeSelect(selectedScope.label);
1099-
setScopeOpened(false);
1100-
setScopeFocusedIndex(-1);
1101-
}
1102-
} else {
1103-
setScopeOpened(!scopeOpened);
1104-
}
1105-
},
1106-
isTabNavigationIgnored: (direction) => {
1107-
if (!scopes) {
1108-
return false;
1109-
}
1110-
1111-
return (
1112-
scopeFocusedIndex === -1 ||
1113-
(direction === 'backward' && scopeFocusedIndex <= 0) ||
1114-
(direction === 'forward' && scopeFocusedIndex >= scopes.length - 1)
1115-
);
1116-
},
1117-
onTabNavigation: (direction) => {
1118-
if (!scopes) return;
1119-
if (direction === 'backward') {
1120-
if (scopeFocusedIndex <= 0) {
1121-
setScopeOpened(false);
1122-
setScopeFocusedIndex(-1);
1123-
} else {
1124-
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
1125-
setScopeFocusedIndex(prevIndex);
1126-
scrollFocusedScopeIntoView(prevIndex);
1127-
}
1128-
} else {
1129-
if (scopeFocusedIndex >= scopes.length - 1) {
1130-
setScopeOpened(false);
1131-
setScopeFocusedIndex(-1);
1132-
} else {
1133-
const nextIndex = Math.min(
1134-
scopeFocusedIndex + 1,
1135-
scopes.length - 1
1136-
);
1137-
setScopeFocusedIndex(nextIndex);
1138-
scrollFocusedScopeIntoView(nextIndex);
1139-
}
1140-
}
1141-
},
1142-
});
1143-
1144-
const onScopeBlur = useEventCallback((e: FocusEvent) => {
1145-
if (e.relatedTarget !== null) {
1146-
if (!scopes) {
1147-
return;
1148-
}
1149-
1150-
const prevIndex = Math.max(scopeFocusedIndex - 1, 0);
1151-
const nextIndex = Math.min(scopeFocusedIndex + 1, scopes.length - 1);
1152-
1153-
if (
1154-
e.relatedTarget.id === getScopeOptionId(prevIndex) ||
1155-
e.relatedTarget.id === getScopeOptionId(nextIndex)
1156-
) {
1157-
// catch keyborad event
1158-
return;
1159-
}
1160-
}
1161-
1162-
setTimeout(() => {
1163-
setScopeOpened(false);
1164-
}, 10);
1165-
});
1166-
11671156
const hasSelection = selected != null;
11681157

11691158
const containerClassNames = classnames(
@@ -1224,15 +1213,14 @@ export const Lookup = createFC<LookupProps, { isFormElement: boolean }>(
12241213
<LookupScopeSelector
12251214
scopes={scopes}
12261215
targetScope={targetScope}
1227-
scopeOpened={scopeOpened}
1228-
scopeFocusedIndex={scopeFocusedIndex}
12291216
disabled={disabled}
12301217
scopeListboxId={scopeListboxId}
12311218
getScopeOptionId={getScopeOptionId}
1232-
onScopeMenuClick={onScopeMenuClick}
1233-
onScopeBlur={onScopeBlur}
1234-
onScopeKeyDown={onScopeKeyDown}
1235-
onScopeSelect={onScopeSelect}
1219+
onScopeMenuClick={onScopeMenuClick_}
1220+
onScopeSelect={(scope) => {
1221+
setTargetScope(scope);
1222+
onScopeSelect_?.(scope);
1223+
}}
12361224
/>
12371225
<div className='slds-combobox_container slds-combobox-addon_end'>
12381226
<div className={comboboxClassNames} ref={elementRef}>

0 commit comments

Comments
 (0)