spacemonkd commented on code in PR #9928:
URL: https://github.com/apache/ozone/pull/9928#discussion_r2954809158
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/multiSelect.tsx:
##########
@@ -97,44 +155,190 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
)}
{isDisabled
? placeholder
- : `${placeholder}: ${selected.filter((opt) => opt.value !==
fixedColumn).length} selected`
+ : `${placeholder}: ${selected.filter((opt) =>
!fixedKeys.includes(opt.value)).length} selected`
}
</components.ValueContainer>
);
};
- const finalStyles = {...selectStyles, ...style ?? {}}
+ // Stable custom Input — suppresses react-select's blur-driven menu close
+ // while the user interacts with the search box inside the menu.
+ const InputComponent = useMemo(
+ () => (({ onBlur, ...inputProps }: any) => {
+ const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
+ if (searchInteracting.current) return;
+ if (onBlur) onBlur(e);
+ };
+ return <input {...inputProps} onBlur={handleBlur} />;
+ }) as React.FC,
+ [] // searchInteracting captured by ref — always current
+ );
Review Comment:
Similarly for this we can move out to it's own component
##########
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/multiSelect.tsx:
##########
@@ -97,44 +155,190 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
)}
{isDisabled
? placeholder
- : `${placeholder}: ${selected.filter((opt) => opt.value !==
fixedColumn).length} selected`
+ : `${placeholder}: ${selected.filter((opt) =>
!fixedKeys.includes(opt.value)).length} selected`
}
</components.ValueContainer>
);
};
- const finalStyles = {...selectStyles, ...style ?? {}}
+ // Stable custom Input — suppresses react-select's blur-driven menu close
+ // while the user interacts with the search box inside the menu.
+ const InputComponent = useMemo(
+ () => (({ onBlur, ...inputProps }: any) => {
+ const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
+ if (searchInteracting.current) return;
+ if (onBlur) onBlur(e);
+ };
+ return <input {...inputProps} onBlur={handleBlur} />;
+ }) as React.FC,
+ [] // searchInteracting captured by ref — always current
+ );
- const fixedOption = fixedColumn ? options.find((opt) => opt.value ===
fixedColumn) : undefined;
- const selectableOptions = fixedColumn ? options.filter((opt) => opt.value
!== fixedColumn) : options;
+ // Stable MenuList — created once, reads current values from stateRef at
+ // call time to avoid stale closures without re-creating the component type.
+ const MenuListComponent = useMemo(
+ () => ({ children, ...menuListProps }: any) => {
+ const {
+ searchTerm,
+ setSearchTerm,
+ showSearch,
+ showSelectAll,
+ selected,
+ selectableOptions,
+ fixedOptions,
+ options,
+ onChange
+ } = stateRef.current;
- return (
+ const allSelected = selectableOptions.length > 0 &&
+ selectableOptions.every((opt: Option) => selected.some((s: Option) =>
s.value === opt.value));
+
+ const handleSelectAll = () => {
+ onChange([...fixedOptions, ...selectableOptions]);
+ };
+
+ const handleUnselectAll = () => {
+ onChange(fixedOptions as Option[]);
+ };
+
+ return (
+ <components.MenuList {...menuListProps}>
+ {showSearch && (
+ <div
+ style={{ padding: '8px 12px' }}
+ onMouseDown={(e) => {
+ searchInteracting.current = true;
+ e.preventDefault();
+ }}
+ onClick={() => {
+ const input = (e: any) =>
e?.target?.closest('[data-search-wrapper]')?.querySelector('input');
+ const el = document.querySelector('[data-search-wrapper]
input') as HTMLInputElement | null;
+ if (el) el.focus();
+ }}
+ onFocus={() => { searchInteracting.current = true; }}
+ onBlur={() => {
+ searchInteracting.current = false;
+ setTimeout(() => {
+ const container = stateRef.current.containerRef.current;
+ if (container &&
!container.contains(document.activeElement)) {
+ stateRef.current.setIsMenuOpen(false);
+ stateRef.current.setSearchTerm('');
+ }
+ }, 150);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Escape') {
+ searchInteracting.current = false;
+ stateRef.current.setIsMenuOpen(false);
+ stateRef.current.setSearchTerm('');
+ }
+ e.stopPropagation();
+ }}
+ data-search-wrapper='true'
+ >
+ <input
+ type='text'
+ placeholder='Search...'
+ value={searchTerm}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)}
+ onClick={(e: React.MouseEvent) => e.stopPropagation()}
+ style={{
+ width: '100%',
+ padding: '6px 8px',
+ borderRadius: '4px',
+ border: '1px solid #d9d9d9',
+ fontSize: '14px',
+ boxSizing: 'border-box',
+ outline: 'none'
+ }}
+ />
+ </div>
+ )}
+ {showSelectAll && (
+ <div
+ style={{
+ padding: '6px 12px',
+ cursor: 'pointer',
+ borderBottom: '1px solid #f0f0f0',
+ display: 'flex',
+ alignItems: 'center'
+ }}
+ onMouseDown={(e) => e.preventDefault()}
+ onClick={() => allSelected ? handleUnselectAll() :
handleSelectAll()}
+ >
+ <input
+ type='checkbox'
+ checked={allSelected}
+ onChange={() => null}
+ style={{ marginRight: '8px', accentColor: '#1AA57A' }}
+ />
+ <label style={{ cursor: 'pointer' }}>
+ {allSelected ? 'Unselect All' : 'Select All'}
+ </label>
+ </div>
+ )}
+ {children}
+ </components.MenuList>
+ );
+ },
+ [] // Stable reference — reads current values from stateRef
+ );
+
+ // Only intercept onMenuClose when showSearch is active so we can keep
+ // the dropdown open while the user interacts with the search box.
+ const handleMenuClose = useCallback(() => {
Review Comment:
Is useCallback required?
This uses stable setters and a ref, so nothing would be changing it's
"identity", a normal function should be fine here
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]