Skip to content

Commit a3571d9

Browse files
committed
Wizard: fix dark mode placeholder in profile selector
The ProfileSelector used a typeahead MenuToggle variant with text filtering. PatternFly's typeahead variant renders an <input> element whose placeholder color is controlled by a CSS variable that does not inherit the dark mode theme, causing the placeholder text to appear white-on-white. Replace the typeahead with a simple button-based dropdown matching the PolicySelector pattern. This sidesteps the dark mode bug entirely by removing the <input> element. Also adds an applying state with a spinner, and sets isPlaceholder on the PolicySelector toggle for consistency.
1 parent 46a51fc commit a3571d9

10 files changed

Lines changed: 158 additions & 186 deletions

File tree

playwright/Customizations/FipsMode.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ test('FIPS switch toggles and persists through save', async ({
7171
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
7272
.check();
7373

74-
const typeToFilter = frame.getByRole('textbox', { name: 'Type to filter' });
75-
76-
await typeToFilter.fill('stig');
74+
await frame.getByTestId('profileSelect').click();
7775
await frame
7876
.getByRole('option', {
7977
name: /Red Hat STIG for Red Hat Enterprise Linux 10/i,

playwright/Customizations/OpenSCAP.spec.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,14 @@ test('Create a blueprint with OpenSCAP customization', async ({
5656
await expect(
5757
frame.getByText('WSL: customization is not supported'),
5858
).toBeVisible();
59-
await expect(
60-
frame.getByRole('textbox', { name: 'Type to filter' }),
61-
).toBeEnabled();
6259
});
6360

6461
await test.step('Select a CIS profile then switch to None', async () => {
6562
await frame
6663
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
6764
.click();
68-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
65+
await expect(frame.getByTestId('profileSelect')).toBeEnabled();
66+
await frame.getByTestId('profileSelect').click();
6967
await frame
7068
.getByRole('option', {
7169
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
@@ -91,7 +89,7 @@ test('Create a blueprint with OpenSCAP customization', async ({
9189
.getByRole('radio', { name: 'Use a default OpenSCAP profile' })
9290
.click();
9391

94-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
92+
await frame.getByTestId('profileSelect').click();
9593
await frame
9694
.getByRole('option', {
9795
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
@@ -143,14 +141,11 @@ test('Create a blueprint with OpenSCAP customization', async ({
143141
await test.step('Edit BP', async () => {
144142
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
145143
await frame.getByRole('button', { name: 'Base settings' }).click();
146-
await frame.getByRole('textbox', { name: 'Type to filter' }).click();
147-
await expect(
148-
frame.getByText(
149-
'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
150-
),
151-
).toBeVisible();
152-
await frame.getByRole('textbox', { name: 'Type to filter' }).clear();
153-
await frame.getByRole('textbox', { name: 'Type to filter' }).fill('cis');
144+
await expect(frame.getByTestId('profileSelect')).toContainText(
145+
'CIS Red Hat Enterprise Linux 9 Benchmark for Level 1 - Server',
146+
{ timeout: 60000 },
147+
);
148+
await frame.getByTestId('profileSelect').click();
154149
await frame
155150
.getByRole('option', {
156151
name: 'CIS Red Hat Enterprise Linux 9 Benchmark for Level 2 - Server',

src/Components/CreateImageWizard/steps/Oscap/components/PolicySelector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ const PolicySelector = ({ isDisabled = false }: PolicySelectorProps) => {
229229
<MenuToggle
230230
ouiaId='compliancePolicySelect'
231231
ref={toggleRef}
232+
isPlaceholder={!policyTitle && !isApplying}
232233
onClick={() => setIsOpen(!isOpen)}
233234
isExpanded={isOpen}
234235
isDisabled={isDisabled || isFetchingPolicies || isApplying}

src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx

Lines changed: 47 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
import React, { useEffect, useState } from 'react';
22

33
import {
4-
Button,
54
FormGroup,
65
MenuToggle,
76
MenuToggleElement,
87
Select,
98
SelectList,
109
SelectOption,
1110
Spinner,
12-
TextInputGroup,
13-
TextInputGroupMain,
14-
TextInputGroupUtilities,
1511
} from '@patternfly/react-core';
16-
import { TimesIcon } from '@patternfly/react-icons';
1712

1813
import {
1914
DistributionProfileItem,
@@ -31,7 +26,6 @@ import {
3126
changeFscMode,
3227
clearKernelAppend,
3328
selectComplianceProfileID,
34-
selectComplianceType,
3529
selectDistribution,
3630
setOscapProfile,
3731
} from '@/store/slices/wizard';
@@ -65,21 +59,13 @@ const ProfileSelector = ({
6559
const release = removeBetaFromRelease(useAppSelector(selectDistribution));
6660
const dispatch = useAppDispatch();
6761
const [isOpen, setIsOpen] = useState(false);
68-
const [inputValue, setInputValue] = useState<string>('');
69-
const [filterValue, setFilterValue] = useState<string>('');
70-
const [selectOptions, setSelectOptions] = useState<
71-
{
72-
id: DistributionProfileItem;
73-
name: string | undefined;
74-
}[]
75-
>([]);
62+
const [isApplying, setIsApplying] = useState(false);
7663
const [profileDetails, setProfileDetails] = useState<
7764
{
7865
id: DistributionProfileItem;
7966
name: string | undefined;
8067
}[]
8168
>([]);
82-
const complianceType = useAppSelector(selectComplianceType);
8369
const prefetchProfile = useBackendPrefetch('getOscapCustomizations');
8470
const {
8571
clearCompliancePackages,
@@ -100,14 +86,6 @@ const ProfileSelector = ({
10086

10187
const [trigger] = useLazyGetOscapCustomizationsQuery();
10288

103-
useEffect(() => {
104-
if (!profileID) {
105-
setInputValue('');
106-
setFilterValue('');
107-
setIsOpen(false);
108-
}
109-
}, [profileID]);
110-
11189
// prefetch the profiles customizations for on-prem
11290
// and save the results to the cache, since the request
11391
// is quite slow
@@ -145,33 +123,16 @@ const ProfileSelector = ({
145123

146124
const resolvedProfiles = await Promise.all(promises);
147125
setProfileDetails(resolvedProfiles);
148-
setSelectOptions(resolvedProfiles);
149126
};
150127

151128
fetchProfileDetails();
152129
}, [profiles, release, trigger]);
153130

154-
useEffect(() => {
155-
if (!filterValue) {
156-
setSelectOptions(profileDetails);
157-
return;
158-
}
159-
const trimmedFilter = filterValue.toLowerCase().trim();
160-
const filtered = profileDetails.filter(({ name }) =>
161-
name?.toLowerCase().includes(trimmedFilter),
162-
);
163-
164-
setSelectOptions(filtered);
165-
if (!isOpen && filtered.length > 0) {
166-
setIsOpen(true);
167-
}
168-
}, [filterValue, profileDetails, isOpen]);
169-
170-
const handleToggle = () => {
171-
if (!isOpen && complianceType === 'openscap') {
131+
const handleToggle = (nextIsOpen: boolean) => {
132+
if (nextIsOpen) {
172133
refetch();
173134
}
174-
setIsOpen(!isOpen);
135+
setIsOpen(nextIsOpen);
175136
};
176137

177138
const handleClear = () => {
@@ -181,53 +142,13 @@ const ProfileSelector = ({
181142
handleServices(undefined);
182143
dispatch(clearKernelAppend());
183144
dispatch(changeFips(false));
184-
setInputValue('');
185-
setFilterValue('');
186-
};
187-
188-
const onInputClick = () => {
189-
if (!isOpen) {
190-
setIsOpen(true);
191-
} else if (!inputValue) {
192-
setIsOpen(false);
193-
}
194-
};
195-
196-
const onTextInputChange = (_event: React.FormEvent, value: string) => {
197-
setInputValue(value);
198-
setFilterValue(value);
199-
200-
if (value !== profileID) {
201-
dispatch(setOscapProfile(undefined));
202-
}
203-
};
204-
205-
const onKeyDown = (event: React.KeyboardEvent) => {
206-
if (event.key === 'Enter') {
207-
event.preventDefault();
208-
209-
if (!isOpen) {
210-
setIsOpen(true);
211-
} else if (selectOptions.length === 1) {
212-
const singleProfile = selectOptions[0];
213-
const selection: OScapSelectOptionValueType = {
214-
profileID: singleProfile.id,
215-
toString: () => singleProfile.name || '',
216-
};
217-
218-
setInputValue(singleProfile.name || '');
219-
setFilterValue('');
220-
applyChanges(selection);
221-
setIsOpen(false);
222-
}
223-
}
224145
};
225146

226147
const applyChanges = (selection: OScapSelectOptionValueType) => {
227148
if (selection.profileID === undefined) {
228-
// handle user has selected 'None' case
229149
handleClear();
230150
} else {
151+
setIsApplying(true);
231152
const oldOscapPackages = currentProfileData?.packages || [];
232153
trigger(
233154
{
@@ -250,7 +171,8 @@ const ProfileSelector = ({
250171
handleKernelAppend(response.kernel?.append);
251172
dispatch(setOscapProfile(selection.profileID));
252173
dispatch(changeFips(response.fips?.enabled || false));
253-
});
174+
})
175+
.finally(() => setIsApplying(false));
254176
}
255177
};
256178

@@ -260,49 +182,56 @@ const ProfileSelector = ({
260182
) => {
261183
if (selection === undefined) return;
262184

263-
setInputValue((selection as OScapSelectOptionValueType['profileID']) || '');
264-
setFilterValue('');
265185
applyChanges(selection as unknown as OScapSelectOptionValueType);
266186
setIsOpen(false);
267187
};
268188

189+
const selectedProfileName = profileID
190+
? profileDetails.find(({ id }) => id === profileID)?.name || profileID
191+
: undefined;
192+
193+
const profileOptions = () => {
194+
if (isFetching) {
195+
return [
196+
<SelectOption key='oscap-loader' value='loader'>
197+
<Spinner size='lg' />
198+
</SelectOption>,
199+
];
200+
}
201+
202+
const res = profileDetails.map(({ id, name }) => (
203+
<SelectOption
204+
key={id}
205+
value={{
206+
profileID: id,
207+
toString: () => name,
208+
}}
209+
isSelected={profileID === id}
210+
>
211+
{name}
212+
</SelectOption>
213+
));
214+
return res;
215+
};
216+
269217
const toggleOpenSCAP = (toggleRef: React.Ref<MenuToggleElement>) => (
270218
<MenuToggle
271219
data-testid='profileSelect'
272220
ref={toggleRef}
273-
variant='typeahead'
274-
onClick={() => setIsOpen(!isOpen)}
221+
isPlaceholder={!selectedProfileName && !isApplying}
222+
onClick={() => handleToggle(!isOpen)}
275223
isExpanded={isOpen}
276-
isDisabled={isDisabled || !isSuccess}
224+
isDisabled={isDisabled || !isSuccess || isApplying}
277225
isFullWidth
226+
style={{ maxWidth: 'none' }}
278227
>
279-
<TextInputGroup isPlain>
280-
<TextInputGroupMain
281-
value={
282-
profileID
283-
? profileDetails.find(({ id }) => id === profileID)?.name ||
284-
profileID
285-
: inputValue
286-
}
287-
onClick={onInputClick}
288-
onChange={onTextInputChange}
289-
onKeyDown={onKeyDown}
290-
autoComplete='off'
291-
placeholder='Select a profile'
292-
isExpanded={isOpen}
293-
/>
294-
295-
{profileID && (
296-
<TextInputGroupUtilities>
297-
<Button
298-
icon={<TimesIcon />}
299-
variant='plain'
300-
onClick={handleClear}
301-
aria-label='Clear input'
302-
/>
303-
</TextInputGroupUtilities>
304-
)}
305-
</TextInputGroup>
228+
{isApplying ? (
229+
<>
230+
<Spinner size='sm' /> Applying profile...
231+
</>
232+
) : (
233+
selectedProfileName || 'Select a profile'
234+
)}
306235
</MenuToggle>
307236
);
308237

@@ -316,35 +245,8 @@ const ProfileSelector = ({
316245
onOpenChange={handleToggle}
317246
toggle={toggleOpenSCAP}
318247
shouldFocusFirstItemOnOpen={false}
319-
popperProps={{
320-
maxWidth: '50vw',
321-
}}
322248
>
323-
<SelectList>
324-
{isFetching && (
325-
<SelectOption value='loader'>
326-
<Spinner size='lg' />
327-
</SelectOption>
328-
)}
329-
{selectOptions.length > 0 &&
330-
selectOptions.map(({ id, name }) => (
331-
<SelectOption
332-
key={id}
333-
value={{
334-
profileID: id,
335-
toString: () => name,
336-
}}
337-
isSelected={profileID === id}
338-
>
339-
{name}
340-
</SelectOption>
341-
))}
342-
{isSuccess && selectOptions.length === 0 && (
343-
<SelectOption isDisabled>
344-
{`No results found for "${filterValue}"`}
345-
</SelectOption>
346-
)}
347-
</SelectList>
249+
<SelectList>{profileOptions()}</SelectList>
348250
</Select>
349251
</FormGroup>
350252
);

0 commit comments

Comments
 (0)