Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions src/form/KaotoForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@
justify-content: space-between;
}

&__suggestions-button {
display: none;
}

/* stylelint-disable-next-line selector-class-pattern */
.pf-v6-c-text-input-group:hover &__suggestions-button,
/* stylelint-disable-next-line selector-class-pattern */
.pf-v6-c-text-input-group:focus-within &__suggestions-button,
/* stylelint-disable-next-line selector-class-pattern */
.pf-v6-c-input-group:hover &__suggestions-button,
/* stylelint-disable-next-line selector-class-pattern */
.pf-v6-c-input-group:focus-within &__suggestions-button,
&__suggestions-button:focus {
display: inline-flex;
}

&__empty {
display: none;
}
Expand Down
10 changes: 8 additions & 2 deletions src/form/KeyValue/KeyValueField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { KeyValueField } from './KeyValueField';

// Mock useSuggestions to control its output
jest.mock('../hooks/suggestions', () => ({
useSuggestions: jest.fn(() => null),
useSuggestions: jest.fn(() => ({
suggestionsMenu: null,
openSuggestions: jest.fn(),
})),
}));

describe('KeyValueField', () => {
Expand Down Expand Up @@ -75,7 +78,10 @@ describe('KeyValueField', () => {
const SuggestionsMenu = () => <div data-testid="suggestions-menu">Suggestions</div>;
const { useSuggestions } = jest.requireMock('../hooks/suggestions');

useSuggestions.mockImplementation(() => <SuggestionsMenu />);
useSuggestions.mockImplementation(() => ({
suggestionsMenu: <SuggestionsMenu />,
openSuggestions: jest.fn(),
}));

const { getByTestId } = render(<KeyValueField {...defaultProps} />);
const input = getByTestId('keyvalue-input');
Expand Down
4 changes: 2 additions & 2 deletions src/form/KeyValue/KeyValueField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const KeyValueField = forwardRef<HTMLInputElement, KeyValueFieldProps>(

useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);

const suggestions = useSuggestions({
const { suggestionsMenu } = useSuggestions({
propName: name,
schema: STRING_SCHEMA,
inputRef,
Expand All @@ -47,7 +47,7 @@ export const KeyValueField = forwardRef<HTMLInputElement, KeyValueFieldProps>(
value={value}
/>

{suggestions}
{suggestionsMenu}
</TextInputGroup>
);
},
Expand Down
30 changes: 29 additions & 1 deletion src/form/__snapshots__/KaotoForm.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ exports[`KaotoForm should validate the model 1`] = `
<span
aria-label="More info for Name field"
class="pf-v6-c-button pf-m-plain pf-m-no-padding"
data-ouia-component-id="OUIA-Generated-Button-plain-7"
data-ouia-component-id="OUIA-Generated-Button-plain-13"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
role="button"
Expand Down Expand Up @@ -119,6 +119,34 @@ exports[`KaotoForm should validate the model 1`] = `
<div
class="pf-v6-c-text-input-group__utilities"
>
<button
aria-label="Open suggestions"
class="pf-v6-c-button pf-m-plain kaoto-form__suggestions-button"
data-ouia-component-id="OUIA-Generated-Button-plain-14"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
data-testid="#.name__open-suggestions-button"
title="Open suggestions (Ctrl+Space)"
type="button"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 352 512"
width="1em"
>
<path
d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z"
/>
</svg>
</span>
</button>
<button
aria-expanded="false"
aria-label="#.name__field-actions"
Expand Down
2 changes: 1 addition & 1 deletion src/form/fields/FieldActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('FieldActions', () => {
fireEvent.click(toggle);
});

expect(wrapper.queryByTestId('testProp__clear')).not.toBeInTheDocument();
expect(toggle).toHaveAttribute('aria-expanded', 'false');
});

it('calls onRemove when Clear is clicked', async () => {
Expand Down
18 changes: 17 additions & 1 deletion src/form/fields/PasswordField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { act, fireEvent, render, waitFor, within } from '@testing-library/react';
import { ReactNode, useState } from 'react';
import { ModelContextProvider } from '../providers/ModelProvider';
import { SchemaProvider } from '../providers/SchemaProvider';
import { SuggestionContext } from '../providers/SuggestionRegistryProvider';
import { ROOT_PATH } from '../utils';
import { PasswordField } from './PasswordField';

const StatefulSuggestionProvider = ({
children,
getProviders,
}: {
children: ReactNode;
getProviders: jest.Mock;
}) => {
const [currentOpenMenu, setCurrentOpenMenu] = useState<string | null>(null);
return (
<SuggestionContext.Provider value={{ getProviders, currentOpenMenu, setCurrentOpenMenu }}>
{children}
</SuggestionContext.Provider>
);
};

describe('PasswordField', () => {
const mockSuggestionProvider = {
id: 'test-provider',
Expand All @@ -19,7 +35,7 @@ describe('PasswordField', () => {

const renderWithSuggestions = (children: React.ReactNode) => {
return render(
<SuggestionContext.Provider value={{ getProviders: getProvidersMock }}>{children}</SuggestionContext.Provider>,
<StatefulSuggestionProvider getProviders={getProvidersMock}>{children}</StatefulSuggestionProvider>,
);
};

Expand Down
7 changes: 5 additions & 2 deletions src/form/fields/PasswordField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SchemaContext } from '../providers/SchemaProvider';
import { isDefined, isRawString } from '../utils';
import { FieldActions } from './FieldActions';
import { FieldWrapper } from './FieldWrapper';
import { SuggestionsButton } from './SuggestionsButton';

export const PasswordField: FunctionComponent<FieldProps> = ({ propName, required, onRemove: onRemoveProps }) => {
const { schema } = useContext(SchemaContext);
Expand Down Expand Up @@ -51,7 +52,7 @@ export const PasswordField: FunctionComponent<FieldProps> = ({ propName, require
[onFieldChange],
);

const suggestions = useSuggestions({
const { suggestionsMenu, openSuggestions } = useSuggestions({
propName,
schema,
inputRef,
Expand Down Expand Up @@ -86,9 +87,11 @@ export const PasswordField: FunctionComponent<FieldProps> = ({ propName, require
ref={inputRef}
/>

{suggestions}
{suggestionsMenu}

<TextInputGroupUtilities>
<SuggestionsButton propName={propName} onClick={openSuggestions} />

<Button
variant="plain"
data-testid={`${propName}__toggle-visibility`}
Expand Down
18 changes: 17 additions & 1 deletion src/form/fields/StringField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { act, fireEvent, render, waitFor, within } from '@testing-library/react';
import { JSONSchema4 } from 'json-schema';
import { ReactNode, useState } from 'react';
import { SuggestionContext } from '../providers';
import { ModelContext, ModelContextProvider } from '../providers/ModelProvider';
import { SchemaProvider } from '../providers/SchemaProvider';
import { ROOT_PATH } from '../utils';
import { StringField } from './StringField';

const StatefulSuggestionProvider = ({
children,
getProviders,
}: {
children: ReactNode;
getProviders: jest.Mock;
}) => {
const [currentOpenMenu, setCurrentOpenMenu] = useState<string | null>(null);
return (
<SuggestionContext.Provider value={{ getProviders, currentOpenMenu, setCurrentOpenMenu }}>
{children}
</SuggestionContext.Provider>
);
};

describe('StringField', () => {
const mockSuggestionProvider = {
id: 'test-provider',
Expand All @@ -20,7 +36,7 @@ describe('StringField', () => {

const renderWithSuggestions = (children: React.ReactNode) => {
return render(
<SuggestionContext.Provider value={{ getProviders: getProvidersMock }}>{children}</SuggestionContext.Provider>,
<StatefulSuggestionProvider getProviders={getProvidersMock}>{children}</StatefulSuggestionProvider>,
);
};

Expand Down
7 changes: 5 additions & 2 deletions src/form/fields/StringField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SchemaContext } from '../providers/SchemaProvider';
import { isDefined, isRawString } from '../utils';
import { FieldActions } from './FieldActions';
import { FieldWrapper } from './FieldWrapper';
import { SuggestionsButton } from './SuggestionsButton';

interface StringFieldProps extends FieldProps {
fieldType?: TextInputGroupMainProps['type'];
Expand Down Expand Up @@ -82,7 +83,7 @@ export const StringField: FunctionComponent<StringFieldProps> = ({
[onFieldChange],
);

const suggestions = useSuggestions({
const { suggestionsMenu, openSuggestions } = useSuggestions({
propName,
schema,
inputRef,
Expand Down Expand Up @@ -118,11 +119,13 @@ export const StringField: FunctionComponent<StringFieldProps> = ({
ref={inputRef}
/>

{suggestions}
{suggestionsMenu}

<TextInputGroupUtilities>
{additionalUtility}

<SuggestionsButton propName={propName} onClick={openSuggestions} />

<FieldActions
propName={propName}
clearAriaLabel={clearButtonAriaLabel}
Expand Down
20 changes: 20 additions & 0 deletions src/form/fields/SuggestionsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Button } from '@patternfly/react-core';
import { LightbulbIcon } from '@patternfly/react-icons';
import { FunctionComponent } from 'react';

interface SuggestionsButtonProps {
propName: string;
onClick: () => void;
}

export const SuggestionsButton: FunctionComponent<SuggestionsButtonProps> = ({ propName, onClick }) => (
<Button
variant="plain"
className="kaoto-form__suggestions-button"
data-testid={`${propName}__open-suggestions-button`}
onClick={onClick}
aria-label="Open suggestions"
title="Open suggestions (Ctrl+Space)"
icon={<LightbulbIcon />}
/>
);
10 changes: 8 additions & 2 deletions src/form/fields/TextAreaField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { TextAreaField } from './TextAreaField';

// Mock useSuggestions to control its output
jest.mock('../hooks/suggestions', () => ({
useSuggestions: jest.fn(() => null),
useSuggestions: jest.fn(() => ({
suggestionsMenu: null,
openSuggestions: jest.fn(),
})),
}));

describe('TextAreaField', () => {
Expand Down Expand Up @@ -117,7 +120,10 @@ describe('TextAreaField', () => {
const SuggestionsMenu = () => <div data-testid="suggestions-menu">Suggestions</div>;
const { useSuggestions } = jest.requireMock('../hooks/suggestions');

useSuggestions.mockImplementation(() => <SuggestionsMenu />);
useSuggestions.mockImplementation(() => ({
suggestionsMenu: <SuggestionsMenu />,
openSuggestions: jest.fn(),
}));

const { getByRole, getByTestId } = render(
<ModelContextProvider model="Value" onPropertyChange={jest.fn()}>
Expand Down
7 changes: 5 additions & 2 deletions src/form/fields/TextAreaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FieldProps } from '../models/typings';
import { SchemaContext } from '../providers/SchemaProvider';
import { isDefined } from '../utils';
import { FieldWrapper } from './FieldWrapper';
import { SuggestionsButton } from './SuggestionsButton';

export const TextAreaField: FunctionComponent<FieldProps> = ({ propName, required, onRemove: onRemoveProps }) => {
const { schema } = useContext(SchemaContext);
Expand All @@ -30,7 +31,7 @@ export const TextAreaField: FunctionComponent<FieldProps> = ({ propName, require
onChange(undefined as unknown as string);
};

const suggestions = useSuggestions({
const { suggestionsMenu, openSuggestions } = useSuggestions({
propName,
schema,
inputRef: textAreaRef,
Expand Down Expand Up @@ -65,10 +66,12 @@ export const TextAreaField: FunctionComponent<FieldProps> = ({ propName, require
ref={textAreaRef}
/>

{suggestions}
{suggestionsMenu}
</InputGroupItem>

<InputGroupItem>
<SuggestionsButton propName={propName} onClick={openSuggestions} />

<Button
variant="plain"
data-testid={`${propName}__clear`}
Expand Down
30 changes: 29 additions & 1 deletion src/form/fields/__snapshots__/PasswordField.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,38 @@ exports[`PasswordField should render 1`] = `
<div
class="pf-v6-c-text-input-group__utilities"
>
<button
aria-label="Open suggestions"
class="pf-v6-c-button pf-m-plain kaoto-form__suggestions-button"
data-ouia-component-id="OUIA-Generated-Button-plain-2"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
data-testid="#__open-suggestions-button"
title="Open suggestions (Ctrl+Space)"
type="button"
>
<span
class="pf-v6-c-button__icon"
>
<svg
aria-hidden="true"
class="pf-v6-svg"
fill="currentColor"
height="1em"
role="img"
viewBox="0 0 352 512"
width="1em"
>
<path
d="M96.06 454.35c.01 6.29 1.87 12.45 5.36 17.69l17.09 25.69a31.99 31.99 0 0 0 26.64 14.28h61.71a31.99 31.99 0 0 0 26.64-14.28l17.09-25.69a31.989 31.989 0 0 0 5.36-17.69l.04-38.35H96.01l.05 38.35zM0 176c0 44.37 16.45 84.85 43.56 115.78 16.52 18.85 42.36 58.23 52.21 91.45.04.26.07.52.11.78h160.24c.04-.26.07-.51.11-.78 9.85-33.22 35.69-72.6 52.21-91.45C335.55 260.85 352 220.37 352 176 352 78.61 272.91-.3 175.45 0 73.44.31 0 82.97 0 176zm176-80c-44.11 0-80 35.89-80 80 0 8.84-7.16 16-16 16s-16-7.16-16-16c0-61.76 50.24-112 112-112 8.84 0 16 7.16 16 16s-7.16 16-16 16z"
/>
</svg>
</span>
</button>
<button
aria-label="Show # value"
class="pf-v6-c-button pf-m-plain"
data-ouia-component-id="OUIA-Generated-Button-plain-2"
data-ouia-component-id="OUIA-Generated-Button-plain-3"
data-ouia-component-type="PF6/Button"
data-ouia-safe="true"
data-testid="#__toggle-visibility"
Expand Down
Loading
Loading