@kwiz/fluentui 1.0.16 → 1.0.19

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 (134) hide show
  1. package/.github/workflows/npm-publish.yml +34 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/package.json +72 -72
  5. package/src/_modules/config.ts +9 -9
  6. package/src/_modules/constants.ts +3 -3
  7. package/src/controls/accordion.tsx +48 -48
  8. package/src/controls/button.tsx +169 -169
  9. package/src/controls/centered.tsx +22 -22
  10. package/src/controls/date.tsx +39 -39
  11. package/src/controls/dropdown.tsx +51 -51
  12. package/src/controls/error-boundary.tsx +41 -41
  13. package/src/controls/field-editor.tsx +40 -40
  14. package/src/controls/file-upload.tsx +67 -67
  15. package/src/controls/horizontal.tsx +34 -34
  16. package/src/controls/input.tsx +60 -60
  17. package/src/controls/kwizoverflow.tsx +103 -103
  18. package/src/controls/list.tsx +117 -117
  19. package/src/controls/loading.tsx +10 -10
  20. package/src/controls/please-wait.tsx +32 -32
  21. package/src/controls/prompt.tsx +96 -96
  22. package/src/controls/search.tsx +65 -65
  23. package/src/controls/section.tsx +51 -51
  24. package/src/controls/svg.tsx +120 -120
  25. package/src/controls/toolbar.tsx +48 -48
  26. package/src/controls/vertical-content.tsx +49 -49
  27. package/src/controls/vertical.tsx +34 -34
  28. package/src/helpers/context.ts +39 -39
  29. package/src/helpers/hooks.tsx +335 -335
  30. package/src/index.ts +26 -26
  31. package/src/styles/styles.ts +87 -87
  32. package/src/styles/theme.ts +90 -90
  33. package/dist/_modules/build.d.ts +0 -2
  34. package/dist/_modules/build.js +0 -3
  35. package/dist/_modules/build.js.map +0 -1
  36. package/dist/_modules/config.d.ts +0 -1
  37. package/dist/_modules/config.js +0 -9
  38. package/dist/_modules/config.js.map +0 -1
  39. package/dist/_modules/constants.d.ts +0 -2
  40. package/dist/_modules/constants.js +0 -3
  41. package/dist/_modules/constants.js.map +0 -1
  42. package/dist/_modules/exports-index.d.ts +0 -1
  43. package/dist/_modules/exports-index.js +0 -2
  44. package/dist/_modules/exports-index.js.map +0 -1
  45. package/dist/controls/accordion.d.ts +0 -13
  46. package/dist/controls/accordion.js +0 -27
  47. package/dist/controls/accordion.js.map +0 -1
  48. package/dist/controls/button.d.ts +0 -28
  49. package/dist/controls/button.js +0 -113
  50. package/dist/controls/button.js.map +0 -1
  51. package/dist/controls/centered.d.ts +0 -5
  52. package/dist/controls/centered.js +0 -14
  53. package/dist/controls/centered.js.map +0 -1
  54. package/dist/controls/date.d.ts +0 -8
  55. package/dist/controls/date.js +0 -32
  56. package/dist/controls/date.js.map +0 -1
  57. package/dist/controls/dropdown.d.ts +0 -29
  58. package/dist/controls/dropdown.js +0 -27
  59. package/dist/controls/dropdown.js.map +0 -1
  60. package/dist/controls/error-boundary.d.ts +0 -23
  61. package/dist/controls/error-boundary.js +0 -33
  62. package/dist/controls/error-boundary.js.map +0 -1
  63. package/dist/controls/exports-index.d.ts +0 -17
  64. package/dist/controls/exports-index.js +0 -18
  65. package/dist/controls/exports-index.js.map +0 -1
  66. package/dist/controls/field-editor.d.ts +0 -13
  67. package/dist/controls/field-editor.js +0 -15
  68. package/dist/controls/field-editor.js.map +0 -1
  69. package/dist/controls/file-upload.d.ts +0 -18
  70. package/dist/controls/file-upload.js +0 -41
  71. package/dist/controls/file-upload.js.map +0 -1
  72. package/dist/controls/horizontal.d.ts +0 -8
  73. package/dist/controls/horizontal.js +0 -23
  74. package/dist/controls/horizontal.js.map +0 -1
  75. package/dist/controls/input.d.ts +0 -13
  76. package/dist/controls/input.js +0 -43
  77. package/dist/controls/input.js.map +0 -1
  78. package/dist/controls/kwizoverflow.d.ts +0 -14
  79. package/dist/controls/kwizoverflow.js +0 -45
  80. package/dist/controls/kwizoverflow.js.map +0 -1
  81. package/dist/controls/list.d.ts +0 -21
  82. package/dist/controls/list.js +0 -72
  83. package/dist/controls/list.js.map +0 -1
  84. package/dist/controls/loading copy.d.ts +0 -5
  85. package/dist/controls/loading copy.js +0 -7
  86. package/dist/controls/loading copy.js.map +0 -1
  87. package/dist/controls/loading.d.ts +0 -5
  88. package/dist/controls/loading.js +0 -7
  89. package/dist/controls/loading.js.map +0 -1
  90. package/dist/controls/please-wait.d.ts +0 -18
  91. package/dist/controls/please-wait.js +0 -16
  92. package/dist/controls/please-wait.js.map +0 -1
  93. package/dist/controls/prompt.d.ts +0 -32
  94. package/dist/controls/prompt.js +0 -31
  95. package/dist/controls/prompt.js.map +0 -1
  96. package/dist/controls/search.d.ts +0 -13
  97. package/dist/controls/search.js +0 -47
  98. package/dist/controls/search.js.map +0 -1
  99. package/dist/controls/section.d.ts +0 -14
  100. package/dist/controls/section.js +0 -27
  101. package/dist/controls/section.js.map +0 -1
  102. package/dist/controls/svg.d.ts +0 -23
  103. package/dist/controls/svg.js +0 -45
  104. package/dist/controls/svg.js.map +0 -1
  105. package/dist/controls/toolbar.d.ts +0 -12
  106. package/dist/controls/toolbar.js +0 -23
  107. package/dist/controls/toolbar.js.map +0 -1
  108. package/dist/controls/vertical-content.d.ts +0 -6
  109. package/dist/controls/vertical-content.js +0 -37
  110. package/dist/controls/vertical-content.js.map +0 -1
  111. package/dist/controls/vertical.d.ts +0 -8
  112. package/dist/controls/vertical.js +0 -23
  113. package/dist/controls/vertical.js.map +0 -1
  114. package/dist/exports-index.d.ts +0 -3
  115. package/dist/exports-index.js +0 -4
  116. package/dist/exports-index.js.map +0 -1
  117. package/dist/helpers/context.d.ts +0 -26
  118. package/dist/helpers/context.js +0 -15
  119. package/dist/helpers/context.js.map +0 -1
  120. package/dist/helpers/hooks.d.ts +0 -62
  121. package/dist/helpers/hooks.js +0 -287
  122. package/dist/helpers/hooks.js.map +0 -1
  123. package/dist/index.d.ts +0 -25
  124. package/dist/index.js +0 -25
  125. package/dist/index.js.map +0 -1
  126. package/dist/styles/exports-index.d.ts +0 -2
  127. package/dist/styles/exports-index.js +0 -3
  128. package/dist/styles/exports-index.js.map +0 -1
  129. package/dist/styles/styles.d.ts +0 -19
  130. package/dist/styles/styles.js +0 -79
  131. package/dist/styles/styles.js.map +0 -1
  132. package/dist/styles/theme.d.ts +0 -6
  133. package/dist/styles/theme.js +0 -77
  134. package/dist/styles/theme.js.map +0 -1
@@ -1,336 +1,336 @@
1
- import { Link, Toast, ToastBody, Toaster, ToastFooter, ToastIntent, ToastTitle, useId, useToastController } from "@fluentui/react-components";
2
- import { IDictionary, isDebug, isFunction, isNotEmptyArray, isNullOrEmptyString, jsonClone, jsonStringify, LoggerLevel, objectsEqual, wrapFunction } from "@kwiz/common";
3
- import { MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
4
- import { GetLogger } from "../_modules/config";
5
- import { IPrompterProps, Prompter } from "../controls/prompt";
6
- import { KnownClassNames } from "../styles/styles";
7
- import { iKWIZFluentContext, useKWIZFluentContext } from "./context";
8
-
9
- const logger = GetLogger("helpers/hooks");
10
- /** Empty array ensures that effect is only run on mount */
11
- export const useEffectOnlyOnMount = [];
12
-
13
- /** set state on steroids. provide promise callback after render, onChange transformer and automatic skip-set when value not changed */
14
- export function useStateEX<ValueType>(initialValue: ValueType, options?: {
15
- onChange?: (newValue: SetStateAction<ValueType>) => SetStateAction<ValueType>;
16
- //will not set state if value did not change
17
- skipUpdateIfSame?: boolean;
18
- //optional, provide a name for better logging
19
- name?: string;
20
- }):
21
- [ValueType, (newValue: SetStateAction<ValueType>) => Promise<ValueType>, MutableRefObject<ValueType>] {
22
- options = options || {};
23
- const name = options.name || '';
24
-
25
- let logger = GetLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`);
26
- logger.setLevel(LoggerLevel.WARN);
27
-
28
- const [value, setValueInState] = useState(initialValue);
29
- const currentValue = useRef(value);
30
-
31
- /** make this a collection in case several callers are awaiting the same propr update */
32
- const resolveState = useRef<((v: ValueType) => void)[]>([]);
33
- const isMounted = useRef(false);
34
-
35
- useEffect(() => {
36
- isMounted.current = true;
37
-
38
- return () => {
39
- isMounted.current = false;
40
- };
41
- }, useEffectOnlyOnMount);
42
-
43
- function resolvePromises() {
44
- if (isNotEmptyArray(resolveState.current)) {
45
- let resolvers = resolveState.current.slice();
46
- resolveState.current = [];//clear
47
- resolvers.map(r => r(currentValue.current));
48
- }
49
- };
50
- useEffect(() => {
51
- resolvePromises();
52
- if (isNotEmptyArray(resolveState.current)) {
53
- logger.log(`resolved after render`);
54
- let resolvers = resolveState.current.slice();
55
- resolveState.current = [];//clear
56
- resolvers.map(r => r(value));
57
- }
58
- }, [value, resolveState.current]);
59
-
60
- let setValueWithCheck = !options.skipUpdateIfSame ? setValueInState : (newValue: ValueType) => {
61
- logger.groupSync('conditional value change', log => {
62
- if (logger.getLevel() === LoggerLevel.VERBOSE) {
63
- log('old: ' + jsonStringify(currentValue.current));
64
- log('new: ' + jsonStringify(newValue));
65
- }
66
- if (!objectsEqual(newValue as object, currentValue.current as object)) {
67
- log(`value changed`);
68
- setValueInState(newValue);
69
- }
70
- else {
71
- log(`value unchanged`);
72
- resolvePromises();
73
- }
74
- });
75
- }
76
-
77
-
78
- let setValueWithEvents = wrapFunction(setValueWithCheck, {
79
- before: newValue => isFunction(options.onChange) ? options.onChange(newValue) : newValue,
80
- after: newValue => currentValue.current = newValue as ValueType
81
- });
82
-
83
- const setValue = useCallback((newState: ValueType) => new Promise<ValueType>(resolve => {
84
- if (!isMounted.current) {
85
- //unmounted may never resolve
86
- logger.log(`resolved without wait`);
87
- resolve(newState);
88
- }
89
- else {
90
- resolveState.current.push(resolve);
91
- setValueWithEvents(newState);
92
- }
93
- }), []);
94
-
95
- return [value, setValue, currentValue];
96
- }
97
- export function useTrackFocus(props: { onFocus: () => void, onLoseFocus: () => void, ref?: MutableRefObject<HTMLElement> }) {
98
- const wrapperDiv = props.ref || useRef<HTMLDivElement>(null);
99
- useEffect(() => {
100
- function focusIn(e: FocusEvent) {
101
- let elm = e.target as HTMLElement;//document.activeElement;
102
- if (wrapperDiv.current) {
103
- while (elm && elm !== wrapperDiv.current) {
104
- elm = elm.parentElement;
105
- }
106
- } else elm = null;
107
- if (wrapperDiv.current && elm === wrapperDiv.current) props.onFocus();
108
- else props.onLoseFocus();
109
- }
110
-
111
- if (wrapperDiv.current) {
112
- if (wrapperDiv.current) wrapperDiv.current.tabIndex = 1;
113
- window.addEventListener("focusin", focusIn);
114
- // Remove event listener on cleanup
115
- return () => window.removeEventListener("focusin", focusIn);
116
- }
117
- }, [wrapperDiv.current]);
118
- return wrapperDiv;
119
- }
120
- export function useWindowSize() {
121
- // Initialize state with undefined width/height so server and client renders match
122
- // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
123
- const [windowSize, setWindowSize] = useState<{
124
- width: number,
125
- height: number
126
- }>({
127
- width: undefined,
128
- height: undefined
129
- });
130
- useEffect(() => {
131
- // Handler to call on window resize
132
- function handleResize() {
133
-
134
- // Set window width/height to state
135
- setWindowSize({
136
- width: window.innerWidth,
137
- height: window.innerHeight
138
- });
139
- }
140
- // Add event listener
141
- window.addEventListener("resize", handleResize);
142
- // Call handler right away so state gets updated with initial window size
143
- handleResize();
144
- // Remove event listener on cleanup
145
- return () => window.removeEventListener("resize", handleResize);
146
- }, useEffectOnlyOnMount);
147
- return windowSize;
148
- }
149
- export function useIsInPrint() {
150
- // Initialize state with false
151
- const [printMode, setPrintMode] = useState<boolean>(false);
152
- useEffect(() => {
153
- function forcePrint(e: KeyboardEvent) {
154
- if (e.ctrlKey && e.shiftKey && e.altKey) {
155
- if (e.key.toLocaleLowerCase() === "q") {
156
- document.body.classList.remove(KnownClassNames.print);
157
- handlePrint(e, false);
158
- }
159
- else {
160
- console.warn('forced print mode - to exit refresh to ctrl+shift+alt+q');
161
- document.body.classList.add(KnownClassNames.print);
162
- handlePrint(e, true);
163
- }
164
- }
165
- }
166
- // Handler to call on printing
167
- function handlePrint(e?: Event, force?: boolean) {
168
- if (force === true) setPrintMode(true);
169
- else if (window.matchMedia) {
170
- var mediaQueryList = window.matchMedia('print');
171
- if (mediaQueryList.matches) {
172
- setPrintMode(true);
173
- } else {
174
- setPrintMode(false);
175
- }
176
- }
177
- }
178
- // Add event listener
179
- window.addEventListener("print", handlePrint);
180
- if (isDebug())
181
- window.addEventListener("keydown", forcePrint);
182
- // Call handler right away so state gets updated with initial printing state
183
- handlePrint();
184
- // Remove event listener on cleanup
185
- return () => {
186
- window.removeEventListener("print", handlePrint);
187
- if (isDebug())
188
- window.removeEventListener("keydown", forcePrint);
189
- };
190
- }, useEffectOnlyOnMount);
191
- return printMode;
192
- }
193
- /** set block message if you want to block nav.
194
- * - call setMessage to add a blocker message
195
- * - call onNav when you have internal navigation (open / close popups)
196
- * - render the navPrompt control to your page
197
- * FYI for page unload, most modern browsers won't show your message but a generic one instead. */
198
- export function useBlockNav() {
199
- const [, setBlockNavMessages, blockNavMessagesRef] = useStateEX<IDictionary<string>>({});
200
- const [prompt, setPrompt] = useStateEX<IPrompterProps>(null);
201
-
202
- const getMessagesArr = useCallback(() => {
203
- return Object.keys(blockNavMessagesRef.current).map(id => blockNavMessagesRef.current[id]);
204
- }, useEffectOnlyOnMount);
205
-
206
- const getMessages = useCallback(() => {
207
- return getMessagesArr().join();
208
- }, useEffectOnlyOnMount);
209
-
210
- const onNav = useCallback((nav: () => void) => {
211
- let messages = getMessagesArr();
212
- if (isNotEmptyArray(messages)) {
213
- //need to release react to re-render the prompt
214
- window.setTimeout(() => {
215
- //prompt, if ok - clear messages and nav.
216
- setPrompt({
217
- okButtonText: "Leave",
218
- cancelButtonText: "Cancel",
219
- title: "Leave page?",
220
- children: messages.length > 1
221
- ? <ul>
222
- {messages.map((m, i) => <li key={`m${i}`}>{m}</li>)}
223
- </ul>
224
- : <p>{messages[0]}</p>,
225
- onCancel: () => setPrompt(null),
226
- onOK: () => {
227
- setPrompt(null);
228
- setBlockNavMessages({});//clear messages
229
- nav();
230
- }
231
- });
232
- }, 1);
233
- }
234
- else nav();
235
- }, useEffectOnlyOnMount);
236
-
237
-
238
- useEffect(() => {
239
- function handleBeforeUnload(e: BeforeUnloadEvent) {
240
- //todo: use blockMessageRef.current so that we don't have to re-register every time message changes.
241
- //otherwise we would have to add blockMessage as a dependency for this useEffect
242
- const message = getMessages();
243
- if (!isNullOrEmptyString(message)) {
244
- e.preventDefault();
245
- e.returnValue = message;
246
- }
247
- }
248
- // Add event listener
249
- window.addEventListener("beforeunload", handleBeforeUnload);
250
- // Remove event listener on cleanup
251
- return () => window.removeEventListener("beforeunload", handleBeforeUnload);
252
- }, useEffectOnlyOnMount);
253
- return {
254
- setMessage: (id: string, message?: string) => {
255
- let current = jsonClone(blockNavMessagesRef.current);
256
- if (isNullOrEmptyString(message))
257
- delete current[id];
258
- else current[id] = message;
259
- if (!objectsEqual(current, blockNavMessagesRef.current))
260
- setBlockNavMessages(current);
261
- },
262
- // clearMessages: () => {
263
- // setBlockNavMessages({});
264
- // },
265
- // getMessages,
266
- // getMessagesArr,
267
- onNav,
268
- navPrompt: prompt ? <Prompter {...prompt} /> : undefined
269
- };
270
- }
271
-
272
- export function useKeyDown(options: {
273
- //default use document
274
- elm?: HTMLElement | Document;
275
- onEnter?: (e: KeyboardEvent) => void;
276
- onEscape?: (e: KeyboardEvent) => void;
277
- onKeyDown?: (e: KeyboardEvent) => void;
278
- }) {
279
- let elm = options.elm || document;
280
-
281
- useEffect(() => {
282
- let handler = (e: KeyboardEvent) => {
283
- if (e.key === "Enter" && isFunction(options.onEnter)) options.onEnter(e);
284
- else if (e.key === "Escape" && isFunction(options.onEscape)) options.onEscape(e);
285
- if (isFunction(options.onKeyDown))
286
- options.onKeyDown(e);
287
- };
288
- elm.addEventListener("keydown", handler);
289
- return () => elm.removeEventListener("keydown", handler);
290
- }, [elm]);
291
- }
292
-
293
-
294
- export function useToast() {
295
- const ctx = useKWIZFluentContext();
296
- const toasterId = useId("toaster");
297
- const { dispatchToast } = useToastController(toasterId);
298
- return {
299
- control: <Toaster mountNode={ctx.mountNode} toasterId={toasterId} />,
300
- dispatch: (info: {
301
- title?: string;
302
- body?: string;
303
- subtitle?: string;
304
- titleAction?: { text: string, onClick: () => void },
305
- footerActions?: { text: string, onClick: () => void }[],
306
- intent?: ToastIntent
307
- }) => {
308
- dispatchToast(<Toast>
309
- {info.title && <ToastTitle action={info.titleAction ? <Link onClick={info.titleAction.onClick}>{info.titleAction.text}</Link> : undefined}>{info.title}</ToastTitle>}
310
- {info.body && <ToastBody subtitle={info.subtitle}>{info.body}</ToastBody>}
311
- {isNotEmptyArray(info.footerActions) &&
312
- <ToastFooter>
313
- {info.footerActions.map((a, i) => <Link key={`l${i}`} onClick={a.onClick}>{a.text}</Link>)}
314
- </ToastFooter>
315
- }
316
- </Toast>, { intent: info.intent || "info" });
317
- }
318
- }
319
- }
320
-
321
- export function useKWIZFluentContextProvider(options: {
322
- root?: React.MutableRefObject<HTMLDivElement>;
323
- ctx?: iKWIZFluentContext;
324
- }) {
325
- let v: iKWIZFluentContext = options && options.ctx || {};
326
- const [kwizFluentContext, setKwizFluentContext] = useState<iKWIZFluentContext>(v);
327
- useEffect(() => {
328
- // ref only updates in useEffect, not in useMemo or anything else.
329
- // we need to set it into state so it will trigger a ui update
330
- setKwizFluentContext({
331
- ...v,
332
- mountNode: options.root.current
333
- });
334
- }, [options.root]);
335
- return kwizFluentContext;
1
+ import { Link, Toast, ToastBody, Toaster, ToastFooter, ToastIntent, ToastTitle, useId, useToastController } from "@fluentui/react-components";
2
+ import { IDictionary, isDebug, isFunction, isNotEmptyArray, isNullOrEmptyString, jsonClone, jsonStringify, LoggerLevel, objectsEqual, wrapFunction } from "@kwiz/common";
3
+ import { MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
4
+ import { GetLogger } from "../_modules/config";
5
+ import { IPrompterProps, Prompter } from "../controls/prompt";
6
+ import { KnownClassNames } from "../styles/styles";
7
+ import { iKWIZFluentContext, useKWIZFluentContext } from "./context";
8
+
9
+ const logger = GetLogger("helpers/hooks");
10
+ /** Empty array ensures that effect is only run on mount */
11
+ export const useEffectOnlyOnMount = [];
12
+
13
+ /** set state on steroids. provide promise callback after render, onChange transformer and automatic skip-set when value not changed */
14
+ export function useStateEX<ValueType>(initialValue: ValueType, options?: {
15
+ onChange?: (newValue: SetStateAction<ValueType>) => SetStateAction<ValueType>;
16
+ //will not set state if value did not change
17
+ skipUpdateIfSame?: boolean;
18
+ //optional, provide a name for better logging
19
+ name?: string;
20
+ }):
21
+ [ValueType, (newValue: SetStateAction<ValueType>) => Promise<ValueType>, MutableRefObject<ValueType>] {
22
+ options = options || {};
23
+ const name = options.name || '';
24
+
25
+ let logger = GetLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`);
26
+ logger.setLevel(LoggerLevel.WARN);
27
+
28
+ const [value, setValueInState] = useState(initialValue);
29
+ const currentValue = useRef(value);
30
+
31
+ /** make this a collection in case several callers are awaiting the same propr update */
32
+ const resolveState = useRef<((v: ValueType) => void)[]>([]);
33
+ const isMounted = useRef(false);
34
+
35
+ useEffect(() => {
36
+ isMounted.current = true;
37
+
38
+ return () => {
39
+ isMounted.current = false;
40
+ };
41
+ }, useEffectOnlyOnMount);
42
+
43
+ function resolvePromises() {
44
+ if (isNotEmptyArray(resolveState.current)) {
45
+ let resolvers = resolveState.current.slice();
46
+ resolveState.current = [];//clear
47
+ resolvers.map(r => r(currentValue.current));
48
+ }
49
+ };
50
+ useEffect(() => {
51
+ resolvePromises();
52
+ if (isNotEmptyArray(resolveState.current)) {
53
+ logger.log(`resolved after render`);
54
+ let resolvers = resolveState.current.slice();
55
+ resolveState.current = [];//clear
56
+ resolvers.map(r => r(value));
57
+ }
58
+ }, [value, resolveState.current]);
59
+
60
+ let setValueWithCheck = !options.skipUpdateIfSame ? setValueInState : (newValue: ValueType) => {
61
+ logger.groupSync('conditional value change', log => {
62
+ if (logger.getLevel() === LoggerLevel.VERBOSE) {
63
+ log('old: ' + jsonStringify(currentValue.current));
64
+ log('new: ' + jsonStringify(newValue));
65
+ }
66
+ if (!objectsEqual(newValue as object, currentValue.current as object)) {
67
+ log(`value changed`);
68
+ setValueInState(newValue);
69
+ }
70
+ else {
71
+ log(`value unchanged`);
72
+ resolvePromises();
73
+ }
74
+ });
75
+ }
76
+
77
+
78
+ let setValueWithEvents = wrapFunction(setValueWithCheck, {
79
+ before: newValue => isFunction(options.onChange) ? options.onChange(newValue) : newValue,
80
+ after: newValue => currentValue.current = newValue as ValueType
81
+ });
82
+
83
+ const setValue = useCallback((newState: ValueType) => new Promise<ValueType>(resolve => {
84
+ if (!isMounted.current) {
85
+ //unmounted may never resolve
86
+ logger.log(`resolved without wait`);
87
+ resolve(newState);
88
+ }
89
+ else {
90
+ resolveState.current.push(resolve);
91
+ setValueWithEvents(newState);
92
+ }
93
+ }), []);
94
+
95
+ return [value, setValue, currentValue];
96
+ }
97
+ export function useTrackFocus(props: { onFocus: () => void, onLoseFocus: () => void, ref?: MutableRefObject<HTMLElement> }) {
98
+ const wrapperDiv = props.ref || useRef<HTMLDivElement>(null);
99
+ useEffect(() => {
100
+ function focusIn(e: FocusEvent) {
101
+ let elm = e.target as HTMLElement;//document.activeElement;
102
+ if (wrapperDiv.current) {
103
+ while (elm && elm !== wrapperDiv.current) {
104
+ elm = elm.parentElement;
105
+ }
106
+ } else elm = null;
107
+ if (wrapperDiv.current && elm === wrapperDiv.current) props.onFocus();
108
+ else props.onLoseFocus();
109
+ }
110
+
111
+ if (wrapperDiv.current) {
112
+ if (wrapperDiv.current) wrapperDiv.current.tabIndex = 1;
113
+ window.addEventListener("focusin", focusIn);
114
+ // Remove event listener on cleanup
115
+ return () => window.removeEventListener("focusin", focusIn);
116
+ }
117
+ }, [wrapperDiv.current]);
118
+ return wrapperDiv;
119
+ }
120
+ export function useWindowSize() {
121
+ // Initialize state with undefined width/height so server and client renders match
122
+ // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
123
+ const [windowSize, setWindowSize] = useState<{
124
+ width: number,
125
+ height: number
126
+ }>({
127
+ width: undefined,
128
+ height: undefined
129
+ });
130
+ useEffect(() => {
131
+ // Handler to call on window resize
132
+ function handleResize() {
133
+
134
+ // Set window width/height to state
135
+ setWindowSize({
136
+ width: window.innerWidth,
137
+ height: window.innerHeight
138
+ });
139
+ }
140
+ // Add event listener
141
+ window.addEventListener("resize", handleResize);
142
+ // Call handler right away so state gets updated with initial window size
143
+ handleResize();
144
+ // Remove event listener on cleanup
145
+ return () => window.removeEventListener("resize", handleResize);
146
+ }, useEffectOnlyOnMount);
147
+ return windowSize;
148
+ }
149
+ export function useIsInPrint() {
150
+ // Initialize state with false
151
+ const [printMode, setPrintMode] = useState<boolean>(false);
152
+ useEffect(() => {
153
+ function forcePrint(e: KeyboardEvent) {
154
+ if (e.ctrlKey && e.shiftKey && e.altKey) {
155
+ if (e.key.toLocaleLowerCase() === "q") {
156
+ document.body.classList.remove(KnownClassNames.print);
157
+ handlePrint(e, false);
158
+ }
159
+ else {
160
+ console.warn('forced print mode - to exit refresh to ctrl+shift+alt+q');
161
+ document.body.classList.add(KnownClassNames.print);
162
+ handlePrint(e, true);
163
+ }
164
+ }
165
+ }
166
+ // Handler to call on printing
167
+ function handlePrint(e?: Event, force?: boolean) {
168
+ if (force === true) setPrintMode(true);
169
+ else if (window.matchMedia) {
170
+ var mediaQueryList = window.matchMedia('print');
171
+ if (mediaQueryList.matches) {
172
+ setPrintMode(true);
173
+ } else {
174
+ setPrintMode(false);
175
+ }
176
+ }
177
+ }
178
+ // Add event listener
179
+ window.addEventListener("print", handlePrint);
180
+ if (isDebug())
181
+ window.addEventListener("keydown", forcePrint);
182
+ // Call handler right away so state gets updated with initial printing state
183
+ handlePrint();
184
+ // Remove event listener on cleanup
185
+ return () => {
186
+ window.removeEventListener("print", handlePrint);
187
+ if (isDebug())
188
+ window.removeEventListener("keydown", forcePrint);
189
+ };
190
+ }, useEffectOnlyOnMount);
191
+ return printMode;
192
+ }
193
+ /** set block message if you want to block nav.
194
+ * - call setMessage to add a blocker message
195
+ * - call onNav when you have internal navigation (open / close popups)
196
+ * - render the navPrompt control to your page
197
+ * FYI for page unload, most modern browsers won't show your message but a generic one instead. */
198
+ export function useBlockNav() {
199
+ const [, setBlockNavMessages, blockNavMessagesRef] = useStateEX<IDictionary<string>>({});
200
+ const [prompt, setPrompt] = useStateEX<IPrompterProps>(null);
201
+
202
+ const getMessagesArr = useCallback(() => {
203
+ return Object.keys(blockNavMessagesRef.current).map(id => blockNavMessagesRef.current[id]);
204
+ }, useEffectOnlyOnMount);
205
+
206
+ const getMessages = useCallback(() => {
207
+ return getMessagesArr().join();
208
+ }, useEffectOnlyOnMount);
209
+
210
+ const onNav = useCallback((nav: () => void) => {
211
+ let messages = getMessagesArr();
212
+ if (isNotEmptyArray(messages)) {
213
+ //need to release react to re-render the prompt
214
+ window.setTimeout(() => {
215
+ //prompt, if ok - clear messages and nav.
216
+ setPrompt({
217
+ okButtonText: "Leave",
218
+ cancelButtonText: "Cancel",
219
+ title: "Leave page?",
220
+ children: messages.length > 1
221
+ ? <ul>
222
+ {messages.map((m, i) => <li key={`m${i}`}>{m}</li>)}
223
+ </ul>
224
+ : <p>{messages[0]}</p>,
225
+ onCancel: () => setPrompt(null),
226
+ onOK: () => {
227
+ setPrompt(null);
228
+ setBlockNavMessages({});//clear messages
229
+ nav();
230
+ }
231
+ });
232
+ }, 1);
233
+ }
234
+ else nav();
235
+ }, useEffectOnlyOnMount);
236
+
237
+
238
+ useEffect(() => {
239
+ function handleBeforeUnload(e: BeforeUnloadEvent) {
240
+ //todo: use blockMessageRef.current so that we don't have to re-register every time message changes.
241
+ //otherwise we would have to add blockMessage as a dependency for this useEffect
242
+ const message = getMessages();
243
+ if (!isNullOrEmptyString(message)) {
244
+ e.preventDefault();
245
+ e.returnValue = message;
246
+ }
247
+ }
248
+ // Add event listener
249
+ window.addEventListener("beforeunload", handleBeforeUnload);
250
+ // Remove event listener on cleanup
251
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
252
+ }, useEffectOnlyOnMount);
253
+ return {
254
+ setMessage: (id: string, message?: string) => {
255
+ let current = jsonClone(blockNavMessagesRef.current);
256
+ if (isNullOrEmptyString(message))
257
+ delete current[id];
258
+ else current[id] = message;
259
+ if (!objectsEqual(current, blockNavMessagesRef.current))
260
+ setBlockNavMessages(current);
261
+ },
262
+ // clearMessages: () => {
263
+ // setBlockNavMessages({});
264
+ // },
265
+ // getMessages,
266
+ // getMessagesArr,
267
+ onNav,
268
+ navPrompt: prompt ? <Prompter {...prompt} /> : undefined
269
+ };
270
+ }
271
+
272
+ export function useKeyDown(options: {
273
+ //default use document
274
+ elm?: HTMLElement | Document;
275
+ onEnter?: (e: KeyboardEvent) => void;
276
+ onEscape?: (e: KeyboardEvent) => void;
277
+ onKeyDown?: (e: KeyboardEvent) => void;
278
+ }) {
279
+ let elm = options.elm || document;
280
+
281
+ useEffect(() => {
282
+ let handler = (e: KeyboardEvent) => {
283
+ if (e.key === "Enter" && isFunction(options.onEnter)) options.onEnter(e);
284
+ else if (e.key === "Escape" && isFunction(options.onEscape)) options.onEscape(e);
285
+ if (isFunction(options.onKeyDown))
286
+ options.onKeyDown(e);
287
+ };
288
+ elm.addEventListener("keydown", handler);
289
+ return () => elm.removeEventListener("keydown", handler);
290
+ }, [elm]);
291
+ }
292
+
293
+
294
+ export function useToast() {
295
+ const ctx = useKWIZFluentContext();
296
+ const toasterId = useId("toaster");
297
+ const { dispatchToast } = useToastController(toasterId);
298
+ return {
299
+ control: <Toaster mountNode={ctx.mountNode} toasterId={toasterId} />,
300
+ dispatch: (info: {
301
+ title?: string;
302
+ body?: string;
303
+ subtitle?: string;
304
+ titleAction?: { text: string, onClick: () => void },
305
+ footerActions?: { text: string, onClick: () => void }[],
306
+ intent?: ToastIntent
307
+ }) => {
308
+ dispatchToast(<Toast>
309
+ {info.title && <ToastTitle action={info.titleAction ? <Link onClick={info.titleAction.onClick}>{info.titleAction.text}</Link> : undefined}>{info.title}</ToastTitle>}
310
+ {info.body && <ToastBody subtitle={info.subtitle}>{info.body}</ToastBody>}
311
+ {isNotEmptyArray(info.footerActions) &&
312
+ <ToastFooter>
313
+ {info.footerActions.map((a, i) => <Link key={`l${i}`} onClick={a.onClick}>{a.text}</Link>)}
314
+ </ToastFooter>
315
+ }
316
+ </Toast>, { intent: info.intent || "info" });
317
+ }
318
+ }
319
+ }
320
+
321
+ export function useKWIZFluentContextProvider(options: {
322
+ root?: React.MutableRefObject<HTMLDivElement>;
323
+ ctx?: iKWIZFluentContext;
324
+ }) {
325
+ let v: iKWIZFluentContext = options && options.ctx || {};
326
+ const [kwizFluentContext, setKwizFluentContext] = useState<iKWIZFluentContext>(v);
327
+ useEffect(() => {
328
+ // ref only updates in useEffect, not in useMemo or anything else.
329
+ // we need to set it into state so it will trigger a ui update
330
+ setKwizFluentContext({
331
+ ...v,
332
+ mountNode: options.root.current
333
+ });
334
+ }, [options.root]);
335
+ return kwizFluentContext;
336
336
  }