@keycloakify/keycloak-ui-shared 26.0.6001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +6 -0
  3. package/keycloak-theme/shared/keycloak-ui-shared/alerts/AlertPanel.tsx +43 -0
  4. package/keycloak-theme/shared/keycloak-ui-shared/alerts/Alerts.tsx +82 -0
  5. package/keycloak-theme/shared/keycloak-ui-shared/buttons/FormSubmitButton.tsx +47 -0
  6. package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +60 -0
  7. package/keycloak-theme/shared/keycloak-ui-shared/context/HelpContext.tsx +30 -0
  8. package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +97 -0
  9. package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +50 -0
  10. package/keycloak-theme/shared/keycloak-ui-shared/continue-cancel/ContinueCancelModal.tsx +75 -0
  11. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormErrorText.tsx +23 -0
  12. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +40 -0
  13. package/keycloak-theme/shared/keycloak-ui-shared/controls/HelpItem.tsx +43 -0
  14. package/keycloak-theme/shared/keycloak-ui-shared/controls/KeycloakSpinner.tsx +12 -0
  15. package/keycloak-theme/shared/keycloak-ui-shared/controls/NumberControl.tsx +93 -0
  16. package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +122 -0
  17. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +71 -0
  18. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordInput.tsx +50 -0
  19. package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +67 -0
  20. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +60 -0
  21. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +75 -0
  22. package/keycloak-theme/shared/keycloak-ui-shared/controls/keycloak-text-area/KeycloakTextArea.tsx +23 -0
  23. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +75 -0
  24. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +109 -0
  25. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +285 -0
  26. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +597 -0
  27. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +86 -0
  28. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx +106 -0
  29. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +92 -0
  30. package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +63 -0
  31. package/keycloak-theme/shared/keycloak-ui-shared/index.ts +1 -0
  32. package/keycloak-theme/shared/keycloak-ui-shared/main.ts +96 -0
  33. package/keycloak-theme/shared/keycloak-ui-shared/masthead/DefaultAvatar.tsx +109 -0
  34. package/keycloak-theme/shared/keycloak-ui-shared/masthead/KeycloakDropdown.tsx +48 -0
  35. package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +161 -0
  36. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormPanel.tsx +29 -0
  37. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormTitle.tsx +28 -0
  38. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +98 -0
  39. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollPanel.tsx +21 -0
  40. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/form-title.module.css +4 -0
  41. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/scroll-form.module.css +8 -0
  42. package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +49 -0
  43. package/keycloak-theme/shared/keycloak-ui-shared/select/SingleSelect.tsx +89 -0
  44. package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +198 -0
  45. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +51 -0
  46. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/MultiInputComponent.tsx +146 -0
  47. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +63 -0
  48. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +109 -0
  49. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +23 -0
  50. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +32 -0
  51. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileFields.tsx +243 -0
  52. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileGroup.tsx +71 -0
  53. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +170 -0
  54. package/keycloak-theme/shared/keycloak-ui-shared/utils/ErrorBoundary.tsx +77 -0
  55. package/keycloak-theme/shared/keycloak-ui-shared/utils/createNamedContext.ts +11 -0
  56. package/keycloak-theme/shared/keycloak-ui-shared/utils/darkMode.ts +19 -0
  57. package/keycloak-theme/shared/keycloak-ui-shared/utils/errors.ts +55 -0
  58. package/keycloak-theme/shared/keycloak-ui-shared/utils/generateId.ts +1 -0
  59. package/keycloak-theme/shared/keycloak-ui-shared/utils/getRuleValue.ts +17 -0
  60. package/keycloak-theme/shared/keycloak-ui-shared/utils/isDefined.ts +3 -0
  61. package/keycloak-theme/shared/keycloak-ui-shared/utils/useFetch.ts +44 -0
  62. package/keycloak-theme/shared/keycloak-ui-shared/utils/useRequiredContext.ts +24 -0
  63. package/keycloak-theme/shared/keycloak-ui-shared/utils/useSetTimeout.ts +40 -0
  64. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStorageItem.ts +51 -0
  65. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStoredState.ts +38 -0
  66. package/package.json +31 -0
@@ -0,0 +1,93 @@
1
+ import {
2
+ NumberInput,
3
+ NumberInputProps,
4
+ ValidatedOptions,
5
+ } from "@patternfly/react-core";
6
+ import {
7
+ Controller,
8
+ ControllerProps,
9
+ FieldPath,
10
+ FieldValues,
11
+ UseControllerProps,
12
+ useFormContext,
13
+ } from "react-hook-form";
14
+
15
+ import { getRuleValue } from "../utils/getRuleValue";
16
+ import { FormLabel } from "./FormLabel";
17
+
18
+ export type NumberControlOption = {
19
+ key: string;
20
+ value: string;
21
+ };
22
+
23
+ export type NumberControlProps<
24
+ T extends FieldValues,
25
+ P extends FieldPath<T> = FieldPath<T>,
26
+ > = Omit<NumberInputProps, "name" | "isRequired" | "required"> &
27
+ UseControllerProps<T, P> & {
28
+ name: string;
29
+ label?: string;
30
+ labelIcon?: string;
31
+ controller: Omit<ControllerProps, "name" | "render">;
32
+ };
33
+
34
+ export const NumberControl = <
35
+ T extends FieldValues,
36
+ P extends FieldPath<T> = FieldPath<T>,
37
+ >({
38
+ name,
39
+ label,
40
+ controller,
41
+ labelIcon,
42
+ ...rest
43
+ }: NumberControlProps<T, P>) => {
44
+ const {
45
+ control,
46
+ formState: { errors },
47
+ } = useFormContext();
48
+
49
+ return (
50
+ <FormLabel
51
+ name={name}
52
+ label={label}
53
+ isRequired={controller.rules?.required === true}
54
+ error={errors[name]}
55
+ labelIcon={labelIcon}
56
+ >
57
+ <Controller
58
+ {...controller}
59
+ name={name}
60
+ control={control}
61
+ render={({ field }) => {
62
+ const required = !!controller.rules?.required;
63
+ const min = getRuleValue(controller.rules?.min);
64
+ const value = field.value ?? controller.defaultValue;
65
+ const setValue = (newValue: number) =>
66
+ field.onChange(
67
+ min !== undefined ? Math.max(newValue, Number(min)) : newValue,
68
+ );
69
+
70
+ return (
71
+ <NumberInput
72
+ {...rest}
73
+ id={name}
74
+ value={value}
75
+ validated={
76
+ errors[name] ? ValidatedOptions.error : ValidatedOptions.default
77
+ }
78
+ required={required}
79
+ min={Number(min)}
80
+ max={Number(controller.rules?.max)}
81
+ onPlus={() => setValue(value + 1)}
82
+ onMinus={() => setValue(value - 1)}
83
+ onChange={(event) => {
84
+ const newValue = Number(event.currentTarget.value);
85
+ setValue(!isNaN(newValue) ? newValue : controller.defaultValue);
86
+ }}
87
+ />
88
+ );
89
+ }}
90
+ />
91
+ </FormLabel>
92
+ );
93
+ };
@@ -0,0 +1,122 @@
1
+ import OrganizationRepresentation from "@keycloak/keycloak-admin-client/lib/defs/organizationRepresentation";
2
+ import { Badge, Chip, ChipGroup } from "@patternfly/react-core";
3
+ import { TableText } from "@patternfly/react-table";
4
+ import { FunctionComponent, PropsWithChildren, ReactNode } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+ import { KeycloakDataTable, LoaderFunction } from "./table/KeycloakDataTable";
7
+
8
+ type OrgDetailLinkProps = {
9
+ link: FunctionComponent<
10
+ PropsWithChildren<{ organization: OrganizationRepresentation }>
11
+ >;
12
+ organization: OrganizationRepresentation;
13
+ };
14
+
15
+ const OrgDetailLink = ({ link, organization }: OrgDetailLinkProps) => {
16
+ const { t } = useTranslation();
17
+ const Component = link;
18
+ return (
19
+ <TableText wrapModifier="truncate">
20
+ <Component organization={organization}>
21
+ {organization.name}
22
+ {!organization.enabled && (
23
+ <Badge
24
+ key={`${organization.id}-disabled`}
25
+ isRead
26
+ className="pf-v5-u-ml-sm"
27
+ >
28
+ {t("disabled")}
29
+ </Badge>
30
+ )}
31
+ </Component>
32
+ </TableText>
33
+ );
34
+ };
35
+
36
+ const Domains = (org: OrganizationRepresentation) => {
37
+ const { t } = useTranslation();
38
+ return (
39
+ <ChipGroup
40
+ numChips={2}
41
+ expandedText={t("hide")}
42
+ collapsedText={t("showRemaining")}
43
+ >
44
+ {org.domains?.map((dn) => {
45
+ const name = typeof dn === "string" ? dn : dn.name;
46
+ return (
47
+ <Chip key={name} isReadOnly>
48
+ {name}
49
+ </Chip>
50
+ );
51
+ })}
52
+ </ChipGroup>
53
+ );
54
+ };
55
+
56
+ type OrganizationTableProps = PropsWithChildren & {
57
+ loader:
58
+ | LoaderFunction<OrganizationRepresentation>
59
+ | OrganizationRepresentation[];
60
+ link: FunctionComponent<
61
+ PropsWithChildren<{ organization: OrganizationRepresentation }>
62
+ >;
63
+ toolbarItem?: ReactNode;
64
+ isPaginated?: boolean;
65
+ onSelect?: (orgs: OrganizationRepresentation[]) => void;
66
+ onDelete?: (org: OrganizationRepresentation) => void;
67
+ deleteLabel?: string;
68
+ };
69
+
70
+ export const OrganizationTable = ({
71
+ loader,
72
+ toolbarItem,
73
+ isPaginated = false,
74
+ onSelect,
75
+ onDelete,
76
+ deleteLabel = "delete",
77
+ link,
78
+ children,
79
+ }: OrganizationTableProps) => {
80
+ const { t } = useTranslation();
81
+
82
+ return (
83
+ <KeycloakDataTable
84
+ loader={loader}
85
+ isPaginated={isPaginated}
86
+ ariaLabelKey="organizationList"
87
+ searchPlaceholderKey="searchOrganization"
88
+ toolbarItem={toolbarItem}
89
+ onSelect={onSelect}
90
+ canSelectAll={onSelect !== undefined}
91
+ actions={
92
+ onDelete
93
+ ? [
94
+ {
95
+ title: t(deleteLabel),
96
+ onRowClick: onDelete,
97
+ },
98
+ ]
99
+ : undefined
100
+ }
101
+ columns={[
102
+ {
103
+ name: "name",
104
+ displayKey: "name",
105
+ cellRenderer: (row) => (
106
+ <OrgDetailLink link={link} organization={row} />
107
+ ),
108
+ },
109
+ {
110
+ name: "domains",
111
+ displayKey: "domains",
112
+ cellRenderer: Domains,
113
+ },
114
+ {
115
+ name: "description",
116
+ displayKey: "description",
117
+ },
118
+ ]}
119
+ emptyState={children}
120
+ />
121
+ );
122
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ FormHelperText,
3
+ HelperText,
4
+ HelperTextItem,
5
+ ValidatedOptions,
6
+ } from "@patternfly/react-core";
7
+ import {
8
+ FieldPath,
9
+ FieldValues,
10
+ PathValue,
11
+ UseControllerProps,
12
+ useController,
13
+ } from "react-hook-form";
14
+ import { FormLabel } from "./FormLabel";
15
+ import { PasswordInput, PasswordInputProps } from "./PasswordInput";
16
+
17
+ export type PasswordControlProps<
18
+ T extends FieldValues,
19
+ P extends FieldPath<T> = FieldPath<T>,
20
+ > = UseControllerProps<T, P> &
21
+ Omit<PasswordInputProps, "name" | "isRequired" | "required"> & {
22
+ label: string;
23
+ labelIcon?: string;
24
+ isDisabled?: boolean;
25
+ helperText?: string;
26
+ };
27
+
28
+ export const PasswordControl = <
29
+ T extends FieldValues,
30
+ P extends FieldPath<T> = FieldPath<T>,
31
+ >(
32
+ props: PasswordControlProps<T, P>,
33
+ ) => {
34
+ const { labelIcon, ...rest } = props;
35
+ const required = !!props.rules?.required;
36
+ const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
37
+
38
+ const { field, fieldState } = useController({
39
+ ...props,
40
+ defaultValue,
41
+ });
42
+
43
+ return (
44
+ <FormLabel
45
+ name={props.name}
46
+ label={props.label}
47
+ labelIcon={labelIcon}
48
+ isRequired={required}
49
+ error={fieldState.error}
50
+ >
51
+ <PasswordInput
52
+ isRequired={required}
53
+ id={props.name}
54
+ data-testid={props.name}
55
+ validated={
56
+ fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
57
+ }
58
+ isDisabled={props.isDisabled}
59
+ {...rest}
60
+ {...field}
61
+ />
62
+ {props.helperText && (
63
+ <FormHelperText>
64
+ <HelperText>
65
+ <HelperTextItem>{props.helperText}</HelperTextItem>
66
+ </HelperText>
67
+ </FormHelperText>
68
+ )}
69
+ </FormLabel>
70
+ );
71
+ };
@@ -0,0 +1,50 @@
1
+ import {
2
+ Button,
3
+ InputGroup,
4
+ InputGroupItem,
5
+ TextInput,
6
+ type TextInputProps,
7
+ } from "@patternfly/react-core";
8
+ import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
9
+ import { MutableRefObject, Ref, forwardRef, useState } from "react";
10
+ import { useTranslation } from "react-i18next";
11
+
12
+ export type PasswordInputProps = TextInputProps & {
13
+ hasReveal?: boolean;
14
+ };
15
+
16
+ const PasswordInputBase = ({
17
+ hasReveal = true,
18
+ innerRef,
19
+ ...rest
20
+ }: PasswordInputProps) => {
21
+ const { t } = useTranslation();
22
+ const [hidePassword, setHidePassword] = useState(true);
23
+ return (
24
+ <InputGroup>
25
+ <InputGroupItem isFill>
26
+ <TextInput
27
+ {...rest}
28
+ type={hidePassword ? "password" : "text"}
29
+ ref={innerRef}
30
+ />
31
+ </InputGroupItem>
32
+ {hasReveal && (
33
+ <Button
34
+ variant="control"
35
+ aria-label={t("showPassword")}
36
+ onClick={() => setHidePassword(!hidePassword)}
37
+ >
38
+ {hidePassword ? <EyeIcon /> : <EyeSlashIcon />}
39
+ </Button>
40
+ )}
41
+ </InputGroup>
42
+ );
43
+ };
44
+
45
+ export const PasswordInput = forwardRef(
46
+ (props: PasswordInputProps, ref: Ref<HTMLInputElement>) => (
47
+ <PasswordInputBase {...props} innerRef={ref as MutableRefObject<any>} />
48
+ ),
49
+ );
50
+ PasswordInput.displayName = "PasswordInput";
@@ -0,0 +1,67 @@
1
+ import {
2
+ Controller,
3
+ FieldValues,
4
+ FieldPath,
5
+ UseControllerProps,
6
+ PathValue,
7
+ useFormContext,
8
+ } from "react-hook-form";
9
+ import { SwitchProps, Switch } from "@patternfly/react-core";
10
+ import { FormLabel } from "./FormLabel";
11
+
12
+ export type SwitchControlProps<
13
+ T extends FieldValues,
14
+ P extends FieldPath<T> = FieldPath<T>,
15
+ > = Omit<SwitchProps, "name" | "defaultValue" | "ref"> &
16
+ UseControllerProps<T, P> & {
17
+ name: string;
18
+ label?: string;
19
+ labelIcon?: string;
20
+ labelOn: string;
21
+ labelOff: string;
22
+ stringify?: boolean;
23
+ };
24
+
25
+ export const SwitchControl = <
26
+ T extends FieldValues,
27
+ P extends FieldPath<T> = FieldPath<T>,
28
+ >({
29
+ labelOn,
30
+ stringify,
31
+ defaultValue,
32
+ labelIcon,
33
+ ...props
34
+ }: SwitchControlProps<T, P>) => {
35
+ const fallbackValue = stringify ? "false" : false;
36
+ const defValue = defaultValue ?? (fallbackValue as PathValue<T, P>);
37
+ const { control } = useFormContext();
38
+ return (
39
+ <FormLabel
40
+ hasNoPaddingTop
41
+ name={props.name}
42
+ isRequired={props.rules?.required === true}
43
+ label={props.label}
44
+ labelIcon={labelIcon}
45
+ >
46
+ <Controller
47
+ control={control}
48
+ name={props.name}
49
+ defaultValue={defValue}
50
+ render={({ field: { onChange, value } }) => (
51
+ <Switch
52
+ {...props}
53
+ id={props.name}
54
+ data-testid={props.name}
55
+ label={labelOn}
56
+ isChecked={stringify ? value === "true" : value}
57
+ onChange={(e, checked) => {
58
+ const value = stringify ? checked.toString() : checked;
59
+ props.onChange?.(e, checked);
60
+ onChange(value);
61
+ }}
62
+ />
63
+ )}
64
+ />
65
+ </FormLabel>
66
+ );
67
+ };
@@ -0,0 +1,60 @@
1
+ import {
2
+ TextArea,
3
+ TextAreaProps,
4
+ ValidatedOptions,
5
+ } from "@patternfly/react-core";
6
+ import {
7
+ FieldPath,
8
+ FieldValues,
9
+ PathValue,
10
+ UseControllerProps,
11
+ useController,
12
+ } from "react-hook-form";
13
+
14
+ import { FormLabel } from "./FormLabel";
15
+
16
+ export type TextAreaControlProps<
17
+ T extends FieldValues,
18
+ P extends FieldPath<T> = FieldPath<T>,
19
+ > = UseControllerProps<T, P> &
20
+ TextAreaProps & {
21
+ label: string;
22
+ labelIcon?: string;
23
+ isDisabled?: boolean;
24
+ };
25
+
26
+ export const TextAreaControl = <
27
+ T extends FieldValues,
28
+ P extends FieldPath<T> = FieldPath<T>,
29
+ >(
30
+ props: TextAreaControlProps<T, P>,
31
+ ) => {
32
+ const required = !!props.rules?.required;
33
+ const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
34
+
35
+ const { field, fieldState } = useController({
36
+ ...props,
37
+ defaultValue,
38
+ });
39
+
40
+ return (
41
+ <FormLabel
42
+ isRequired={required}
43
+ label={props.label}
44
+ labelIcon={props.labelIcon}
45
+ name={props.name}
46
+ error={fieldState.error}
47
+ >
48
+ <TextArea
49
+ isRequired={required}
50
+ id={props.name}
51
+ data-testid={props.name}
52
+ validated={
53
+ fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
54
+ }
55
+ isDisabled={props.isDisabled}
56
+ {...field}
57
+ />
58
+ </FormLabel>
59
+ );
60
+ };
@@ -0,0 +1,75 @@
1
+ import {
2
+ FormHelperText,
3
+ HelperText,
4
+ HelperTextItem,
5
+ TextInput,
6
+ TextInputProps,
7
+ ValidatedOptions,
8
+ } from "@patternfly/react-core";
9
+ import { ReactNode } from "react";
10
+ import {
11
+ FieldPath,
12
+ FieldValues,
13
+ PathValue,
14
+ UseControllerProps,
15
+ useController,
16
+ } from "react-hook-form";
17
+
18
+ import { FormLabel } from "./FormLabel";
19
+
20
+ export type TextControlProps<
21
+ T extends FieldValues,
22
+ P extends FieldPath<T> = FieldPath<T>,
23
+ > = UseControllerProps<T, P> &
24
+ Omit<TextInputProps, "name" | "isRequired" | "required"> & {
25
+ label: string;
26
+ labelIcon?: string | ReactNode;
27
+ isDisabled?: boolean;
28
+ helperText?: string;
29
+ "data-testid"?: string;
30
+ };
31
+
32
+ export const TextControl = <
33
+ T extends FieldValues,
34
+ P extends FieldPath<T> = FieldPath<T>,
35
+ >(
36
+ props: TextControlProps<T, P>,
37
+ ) => {
38
+ const { labelIcon, helperText, ...rest } = props;
39
+ const required = !!props.rules?.required;
40
+ const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
41
+
42
+ const { field, fieldState } = useController({
43
+ ...props,
44
+ defaultValue,
45
+ });
46
+
47
+ return (
48
+ <FormLabel
49
+ name={props.name}
50
+ label={props.label}
51
+ labelIcon={labelIcon}
52
+ isRequired={required}
53
+ error={fieldState.error}
54
+ >
55
+ <TextInput
56
+ isRequired={required}
57
+ id={props.name}
58
+ data-testid={props["data-testid"] || props.name}
59
+ validated={
60
+ fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
61
+ }
62
+ isDisabled={props.isDisabled}
63
+ {...rest}
64
+ {...field}
65
+ />
66
+ {helperText && (
67
+ <FormHelperText>
68
+ <HelperText>
69
+ <HelperTextItem>{helperText}</HelperTextItem>
70
+ </HelperText>
71
+ </FormHelperText>
72
+ )}
73
+ </FormLabel>
74
+ );
75
+ };
@@ -0,0 +1,23 @@
1
+ // TODO: Remove code once the following issue has been fixed:
2
+ // https://github.com/patternfly/patternfly-react/issues/10192
3
+ import { TextArea } from "@patternfly/react-core";
4
+ import {
5
+ ComponentProps,
6
+ HTMLProps,
7
+ type ForwardRefExoticComponent,
8
+ type RefAttributes,
9
+ } from "react";
10
+
11
+ // PatternFly changes the signature of the 'onFocus' and 'onBlur' handlers for textarea elements.
12
+ // This causes issues with React Hook Form as it expects the default signature for a textarea element.
13
+ // So we have to create this wrapper component that takes care of converting these signatures for us.
14
+
15
+ export type KeycloakTextAreaProps = Omit<
16
+ ComponentProps<typeof TextArea>,
17
+ "onFocus" | "onBlur"
18
+ > &
19
+ Pick<HTMLProps<HTMLTextAreaElement>, "onFocus" | "onBlur">;
20
+
21
+ export const KeycloakTextArea = TextArea as ForwardRefExoticComponent<
22
+ KeycloakTextAreaProps & RefAttributes<HTMLTextAreaElement>
23
+ >;
@@ -0,0 +1,75 @@
1
+ import { ChipGroupProps, SelectProps } from "@patternfly/react-core";
2
+ import {
3
+ ControllerProps,
4
+ FieldPath,
5
+ FieldValues,
6
+ UseControllerProps,
7
+ } from "react-hook-form";
8
+ import { SingleSelectControl } from "./SingleSelectControl";
9
+ import { TypeaheadSelectControl } from "./TypeaheadSelectControl";
10
+
11
+ type Variant = `${SelectVariant}`;
12
+
13
+ export enum SelectVariant {
14
+ single = "single",
15
+ typeahead = "typeahead",
16
+ typeaheadMulti = "typeaheadMulti",
17
+ }
18
+
19
+ export type SelectControlOption = {
20
+ key: string;
21
+ value: string;
22
+ };
23
+
24
+ export type OptionType = string[] | SelectControlOption[];
25
+
26
+ export type SelectControlProps<
27
+ T extends FieldValues,
28
+ P extends FieldPath<T> = FieldPath<T>,
29
+ > = Omit<
30
+ SelectProps,
31
+ | "name"
32
+ | "toggle"
33
+ | "selections"
34
+ | "onSelect"
35
+ | "onClear"
36
+ | "isOpen"
37
+ | "onFilter"
38
+ | "variant"
39
+ > &
40
+ UseControllerProps<T, P> & {
41
+ name: string;
42
+ label?: string;
43
+ options: OptionType;
44
+ labelIcon?: string;
45
+ controller: Omit<ControllerProps, "name" | "render">;
46
+ onFilter?: (value: string) => void;
47
+ variant?: Variant;
48
+ isDisabled?: boolean;
49
+ menuAppendTo?: string;
50
+ placeholderText?: string;
51
+ chipGroupProps?: ChipGroupProps;
52
+ };
53
+
54
+ export const isSelectBasedOptions = (
55
+ options: OptionType,
56
+ ): options is SelectControlOption[] => typeof options[0] !== "string";
57
+
58
+ export const isString = (
59
+ option: SelectControlOption | string,
60
+ ): option is string => typeof option === "string";
61
+ export const key = (option: SelectControlOption | string) =>
62
+ isString(option) ? option : option.key;
63
+
64
+ export const SelectControl = <
65
+ T extends FieldValues,
66
+ P extends FieldPath<T> = FieldPath<T>,
67
+ >({
68
+ variant = SelectVariant.single,
69
+ ...rest
70
+ }: SelectControlProps<T, P>) =>
71
+ variant === SelectVariant.single ? (
72
+ <SingleSelectControl {...rest} />
73
+ ) : (
74
+ <TypeaheadSelectControl {...rest} variant={variant} />
75
+ );