@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,109 @@
1
+ import {
2
+ MenuToggle,
3
+ MenuToggleStatus,
4
+ Select,
5
+ SelectList,
6
+ SelectOption,
7
+ } from "@patternfly/react-core";
8
+ import { get } from "lodash-es";
9
+ import { useState } from "react";
10
+ import {
11
+ Controller,
12
+ FieldPath,
13
+ FieldValues,
14
+ useFormContext,
15
+ } from "react-hook-form";
16
+ import { getRuleValue } from "../../utils/getRuleValue";
17
+ import { FormLabel } from "../FormLabel";
18
+ import {
19
+ SelectControlProps,
20
+ isSelectBasedOptions,
21
+ isString,
22
+ key,
23
+ } from "./SelectControl";
24
+
25
+ export const SingleSelectControl = <
26
+ T extends FieldValues,
27
+ P extends FieldPath<T> = FieldPath<T>,
28
+ >({
29
+ id,
30
+ name,
31
+ label,
32
+ options,
33
+ controller,
34
+ labelIcon,
35
+ ...rest
36
+ }: SelectControlProps<T, P>) => {
37
+ const {
38
+ control,
39
+ formState: { errors },
40
+ } = useFormContext();
41
+ const [open, setOpen] = useState(false);
42
+ const required = getRuleValue(controller.rules?.required) === true;
43
+
44
+ return (
45
+ <FormLabel
46
+ name={name}
47
+ label={label}
48
+ isRequired={required}
49
+ error={get(errors, name)}
50
+ labelIcon={labelIcon}
51
+ >
52
+ <Controller
53
+ {...controller}
54
+ name={name}
55
+ control={control}
56
+ render={({ field: { onChange, value } }) => (
57
+ <Select
58
+ {...rest}
59
+ onClick={() => setOpen(!open)}
60
+ onOpenChange={() => setOpen(false)}
61
+ selected={
62
+ isSelectBasedOptions(options)
63
+ ? options
64
+ .filter((o) =>
65
+ Array.isArray(value)
66
+ ? value.includes(o.key)
67
+ : value === o.key,
68
+ )
69
+ .map((o) => o.value)
70
+ : value
71
+ }
72
+ toggle={(ref) => (
73
+ <MenuToggle
74
+ id={id || name.slice(name.lastIndexOf(".") + 1)}
75
+ ref={ref}
76
+ onClick={() => setOpen(!open)}
77
+ isExpanded={open}
78
+ isFullWidth
79
+ status={get(errors, name) ? MenuToggleStatus.danger : undefined}
80
+ aria-label="toggle"
81
+ >
82
+ {isSelectBasedOptions(options)
83
+ ? options.find(
84
+ (o) =>
85
+ o.key === (Array.isArray(value) ? value[0] : value),
86
+ )?.value
87
+ : value}
88
+ </MenuToggle>
89
+ )}
90
+ onSelect={(_event, v) => {
91
+ const option = v?.toString();
92
+ onChange(Array.isArray(value) ? [option] : option);
93
+ setOpen(false);
94
+ }}
95
+ isOpen={open}
96
+ >
97
+ <SelectList>
98
+ {options.map((option) => (
99
+ <SelectOption key={key(option)} value={key(option)}>
100
+ {isString(option) ? option : option.value}
101
+ </SelectOption>
102
+ ))}
103
+ </SelectList>
104
+ </Select>
105
+ )}
106
+ />
107
+ </FormLabel>
108
+ );
109
+ };
@@ -0,0 +1,285 @@
1
+ import {
2
+ Button,
3
+ Chip,
4
+ ChipGroup,
5
+ MenuToggle,
6
+ MenuToggleStatus,
7
+ Select,
8
+ SelectList,
9
+ SelectOption,
10
+ TextInputGroup,
11
+ TextInputGroupMain,
12
+ TextInputGroupUtilities,
13
+ } from "@patternfly/react-core";
14
+ import { TimesIcon } from "@patternfly/react-icons";
15
+ import { get } from "lodash-es";
16
+ import { useMemo, useRef, useState } from "react";
17
+ import {
18
+ Controller,
19
+ ControllerRenderProps,
20
+ FieldPath,
21
+ FieldValues,
22
+ useFormContext,
23
+ } from "react-hook-form";
24
+ import { getRuleValue } from "../../utils/getRuleValue";
25
+ import { FormLabel } from "../FormLabel";
26
+ import {
27
+ SelectControlOption,
28
+ SelectControlProps,
29
+ SelectVariant,
30
+ isSelectBasedOptions,
31
+ isString,
32
+ key,
33
+ } from "./SelectControl";
34
+
35
+ const getValue = (option: SelectControlOption | string) =>
36
+ isString(option) ? option : option.value;
37
+
38
+ export const TypeaheadSelectControl = <
39
+ T extends FieldValues,
40
+ P extends FieldPath<T> = FieldPath<T>,
41
+ >({
42
+ id,
43
+ name,
44
+ label,
45
+ options,
46
+ controller,
47
+ labelIcon,
48
+ placeholderText,
49
+ onFilter,
50
+ variant,
51
+ ...rest
52
+ }: SelectControlProps<T, P>) => {
53
+ const {
54
+ control,
55
+ formState: { errors },
56
+ } = useFormContext();
57
+ const [open, setOpen] = useState(false);
58
+ const [filterValue, setFilterValue] = useState("");
59
+ const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
60
+ const textInputRef = useRef<HTMLInputElement>();
61
+ const required = getRuleValue(controller.rules?.required) === true;
62
+
63
+ const filteredOptions = options.filter((option) =>
64
+ getValue(option).toLowerCase().startsWith(filterValue.toLowerCase()),
65
+ );
66
+
67
+ const convert = useMemo(
68
+ () =>
69
+ filteredOptions.map((option, index) => (
70
+ <SelectOption
71
+ key={key(option)}
72
+ value={key(option)}
73
+ isFocused={focusedItemIndex === index}
74
+ >
75
+ {getValue(option)}
76
+ </SelectOption>
77
+ )),
78
+ [focusedItemIndex, filteredOptions],
79
+ );
80
+
81
+ const onInputKeyDown = (
82
+ event: React.KeyboardEvent<HTMLDivElement>,
83
+ field: ControllerRenderProps<FieldValues, string>,
84
+ ) => {
85
+ const focusedItem = filteredOptions[focusedItemIndex];
86
+ setOpen(true);
87
+
88
+ switch (event.key) {
89
+ case "Enter": {
90
+ event.preventDefault();
91
+
92
+ if (variant !== SelectVariant.typeaheadMulti) {
93
+ setFilterValue(getValue(focusedItem));
94
+ } else {
95
+ setFilterValue("");
96
+ }
97
+
98
+ field.onChange(
99
+ Array.isArray(field.value)
100
+ ? [...field.value, key(focusedItem)]
101
+ : key(focusedItem),
102
+ );
103
+ setOpen(false);
104
+ setFocusedItemIndex(0);
105
+
106
+ break;
107
+ }
108
+ case "Tab":
109
+ case "Escape": {
110
+ setOpen(false);
111
+ field.onChange(undefined);
112
+ break;
113
+ }
114
+ case "Backspace": {
115
+ if (variant === SelectVariant.typeahead) {
116
+ field.onChange("");
117
+ }
118
+ break;
119
+ }
120
+ case "ArrowUp":
121
+ case "ArrowDown": {
122
+ event.preventDefault();
123
+
124
+ let indexToFocus = 0;
125
+
126
+ if (event.key === "ArrowUp") {
127
+ if (focusedItemIndex === 0) {
128
+ indexToFocus = options.length - 1;
129
+ } else {
130
+ indexToFocus = focusedItemIndex - 1;
131
+ }
132
+ }
133
+
134
+ if (event.key === "ArrowDown") {
135
+ if (focusedItemIndex === options.length - 1) {
136
+ indexToFocus = 0;
137
+ } else {
138
+ indexToFocus = focusedItemIndex + 1;
139
+ }
140
+ }
141
+
142
+ setFocusedItemIndex(indexToFocus);
143
+ break;
144
+ }
145
+ }
146
+ };
147
+
148
+ return (
149
+ <FormLabel
150
+ name={name}
151
+ label={label}
152
+ isRequired={required}
153
+ error={get(errors, name)}
154
+ labelIcon={labelIcon}
155
+ >
156
+ <Controller
157
+ {...controller}
158
+ name={name}
159
+ control={control}
160
+ render={({ field }) => (
161
+ <Select
162
+ {...rest}
163
+ onClick={() => setOpen(!open)}
164
+ onOpenChange={() => setOpen(false)}
165
+ selected={
166
+ isSelectBasedOptions(options)
167
+ ? options
168
+ .filter((o) =>
169
+ Array.isArray(field.value)
170
+ ? field.value.includes(o.key)
171
+ : field.value === o.key,
172
+ )
173
+ .map((o) => o.value)
174
+ : field.value
175
+ }
176
+ toggle={(ref) => (
177
+ <MenuToggle
178
+ ref={ref}
179
+ id={id || name.slice(name.lastIndexOf(".") + 1)}
180
+ variant="typeahead"
181
+ onClick={() => setOpen(!open)}
182
+ isExpanded={open}
183
+ isFullWidth
184
+ status={get(errors, name) ? MenuToggleStatus.danger : undefined}
185
+ >
186
+ <TextInputGroup isPlain>
187
+ <TextInputGroupMain
188
+ placeholder={placeholderText}
189
+ value={
190
+ variant === SelectVariant.typeahead && field.value
191
+ ? isSelectBasedOptions(options)
192
+ ? options.find(
193
+ (o) =>
194
+ o.key ===
195
+ (Array.isArray(field.value)
196
+ ? field.value[0]
197
+ : field.value),
198
+ )?.value
199
+ : field.value
200
+ : filterValue
201
+ }
202
+ onClick={() => setOpen(!open)}
203
+ onChange={(_, value) => {
204
+ setFilterValue(value);
205
+ onFilter?.(value);
206
+ }}
207
+ onKeyDown={(event) => onInputKeyDown(event, field)}
208
+ autoComplete="off"
209
+ innerRef={textInputRef}
210
+ role="combobox"
211
+ isExpanded={open}
212
+ aria-controls="select-typeahead-listbox"
213
+ >
214
+ {variant === SelectVariant.typeaheadMulti &&
215
+ Array.isArray(field.value) && (
216
+ <ChipGroup aria-label="Current selections">
217
+ {field.value.map(
218
+ (selection: string, index: number) => (
219
+ <Chip
220
+ key={index}
221
+ onClick={(ev) => {
222
+ ev.stopPropagation();
223
+ field.onChange(
224
+ field.value.filter(
225
+ (item: string) => item !== key(selection),
226
+ ),
227
+ );
228
+ }}
229
+ >
230
+ {isSelectBasedOptions(options)
231
+ ? options.find((o) => selection === o.key)
232
+ ?.value
233
+ : getValue(selection)}
234
+ </Chip>
235
+ ),
236
+ )}
237
+ </ChipGroup>
238
+ )}
239
+ </TextInputGroupMain>
240
+ <TextInputGroupUtilities>
241
+ {(!!filterValue || field.value) && (
242
+ <Button
243
+ variant="plain"
244
+ onClick={() => {
245
+ setFilterValue("");
246
+ field.onChange("");
247
+ textInputRef?.current?.focus();
248
+ }}
249
+ aria-label="Clear input value"
250
+ >
251
+ <TimesIcon aria-hidden />
252
+ </Button>
253
+ )}
254
+ </TextInputGroupUtilities>
255
+ </TextInputGroup>
256
+ </MenuToggle>
257
+ )}
258
+ onSelect={(event, v) => {
259
+ event?.stopPropagation();
260
+ const option = v?.toString();
261
+ if (
262
+ variant === SelectVariant.typeaheadMulti &&
263
+ Array.isArray(field.value)
264
+ ) {
265
+ if (field.value.includes(option)) {
266
+ field.onChange(
267
+ field.value.filter((item: string) => item !== option),
268
+ );
269
+ } else {
270
+ field.onChange([...field.value, option]);
271
+ }
272
+ } else {
273
+ field.onChange(Array.isArray(field.value) ? [option] : option);
274
+ setOpen(false);
275
+ }
276
+ }}
277
+ isOpen={open}
278
+ >
279
+ <SelectList>{convert}</SelectList>
280
+ </Select>
281
+ )}
282
+ />
283
+ </FormLabel>
284
+ );
285
+ };