@kwiz/fluentui 1.0.16 → 1.0.19

Sign up to get free protection for your applications and to get access to all the features.
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
  }