@kwiz/fluentui 1.0.73 → 1.0.75

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 (92) hide show
  1. package/.github/workflows/npm-publish.yml +24 -24
  2. package/LICENSE +21 -21
  3. package/README.md +53 -53
  4. package/dist/@types/forwardRef.d.ts +0 -0
  5. package/dist/@types/forwardRef.js +1 -0
  6. package/dist/@types/forwardRef.js.map +1 -0
  7. package/dist/controls/error-boundary copy.d.ts +23 -0
  8. package/dist/controls/error-boundary copy.js +33 -0
  9. package/dist/controls/error-boundary copy.js.map +1 -0
  10. package/dist/controls/menu.js +2 -2
  11. package/dist/controls/menu.js.map +1 -1
  12. package/dist/controls/search.js +19 -11
  13. package/dist/controls/search.js.map +1 -1
  14. package/dist/controls/svg.js +21 -21
  15. package/dist/controls/svg.js.map +1 -1
  16. package/dist/helpers/common.d.ts +4 -0
  17. package/dist/helpers/common.js +2 -0
  18. package/dist/helpers/common.js.map +1 -0
  19. package/dist/helpers/context.d.ts +26 -0
  20. package/dist/helpers/context.js +15 -0
  21. package/dist/helpers/context.js.map +1 -0
  22. package/dist/helpers/drag-drop/exports.d.ts +12 -0
  23. package/dist/helpers/drag-drop/exports.js +3 -0
  24. package/dist/helpers/drag-drop/exports.js.map +1 -0
  25. package/dist/helpers/exports.d.ts +7 -0
  26. package/dist/helpers/exports.js +8 -0
  27. package/dist/helpers/exports.js.map +1 -0
  28. package/dist/helpers/use-editable-control.d.ts +1 -1
  29. package/dist/helpers/use-editable-control.js.map +1 -1
  30. package/package.json +85 -84
  31. package/src/_modules/config.ts +9 -9
  32. package/src/_modules/constants.ts +3 -3
  33. package/src/controls/ColorPickerDialog.tsx +83 -83
  34. package/src/controls/accordion.tsx +62 -62
  35. package/src/controls/button.tsx +180 -180
  36. package/src/controls/canvas/CustomEventTargetBase.ts +32 -32
  37. package/src/controls/canvas/DrawPad.tsx +296 -296
  38. package/src/controls/canvas/DrawPadManager.ts +694 -694
  39. package/src/controls/canvas/bezier.ts +109 -109
  40. package/src/controls/canvas/point.ts +44 -44
  41. package/src/controls/card-list.tsx +31 -31
  42. package/src/controls/card.tsx +77 -77
  43. package/src/controls/centered.tsx +14 -14
  44. package/src/controls/date.tsx +87 -87
  45. package/src/controls/diagram-picker.tsx +96 -96
  46. package/src/controls/divider.tsx +15 -15
  47. package/src/controls/dropdown.tsx +66 -66
  48. package/src/controls/error-boundary.tsx +41 -41
  49. package/src/controls/field-editor.tsx +42 -42
  50. package/src/controls/file-upload.tsx +155 -155
  51. package/src/controls/horizontal.tsx +48 -48
  52. package/src/controls/html-editor/editor.tsx +182 -182
  53. package/src/controls/index.ts +33 -33
  54. package/src/controls/input.tsx +160 -160
  55. package/src/controls/kwizoverflow.tsx +106 -106
  56. package/src/controls/list.tsx +119 -119
  57. package/src/controls/loading.tsx +10 -10
  58. package/src/controls/menu.tsx +173 -173
  59. package/src/controls/merge-text.tsx +126 -126
  60. package/src/controls/please-wait.tsx +32 -32
  61. package/src/controls/progress-bar.tsx +109 -109
  62. package/src/controls/prompt.tsx +121 -121
  63. package/src/controls/qrcode.tsx +36 -36
  64. package/src/controls/search.tsx +71 -61
  65. package/src/controls/section.tsx +133 -133
  66. package/src/controls/svg.tsx +138 -138
  67. package/src/controls/toolbar.tsx +46 -46
  68. package/src/controls/vertical-content.tsx +49 -49
  69. package/src/controls/vertical.tsx +42 -42
  70. package/src/helpers/block-nav.tsx +88 -88
  71. package/src/helpers/context-const.ts +29 -29
  72. package/src/helpers/context-export.tsx +77 -77
  73. package/src/helpers/context-internal.ts +13 -13
  74. package/src/helpers/drag-drop/drag-drop-container.tsx +53 -53
  75. package/src/helpers/drag-drop/drag-drop-context-internal.tsx +9 -9
  76. package/src/helpers/drag-drop/drag-drop-context.tsx +61 -61
  77. package/src/helpers/drag-drop/drag-drop.types.ts +21 -21
  78. package/src/helpers/drag-drop/index.ts +12 -12
  79. package/src/helpers/drag-drop/readme.md +75 -75
  80. package/src/helpers/drag-drop/use-draggable.ts +47 -47
  81. package/src/helpers/drag-drop/use-droppable.ts +38 -38
  82. package/src/helpers/forwardRef.ts +7 -7
  83. package/src/helpers/hooks-events.ts +149 -149
  84. package/src/helpers/hooks.tsx +141 -141
  85. package/src/helpers/index.ts +8 -8
  86. package/src/helpers/use-alerts.tsx +74 -74
  87. package/src/helpers/use-editable-control.tsx +37 -37
  88. package/src/helpers/use-toast.tsx +29 -29
  89. package/src/index.ts +2 -2
  90. package/src/styles/index.ts +1 -1
  91. package/src/styles/styles.ts +104 -104
  92. package/src/styles/theme.ts +90 -90
@@ -1,120 +1,120 @@
1
- import { makeStyles, tokens } from '@fluentui/react-components';
2
- import { LOGO_BLUE_SQUARE, LOGO_WHITE_SQUARE, isNullOrUndefined, isString } from '@kwiz/common';
3
- import React from 'react';
4
- import { useKWIZFluentContext } from '../helpers/context-internal';
5
- import { KnownClassNames, mixins } from '../styles/styles';
6
- import { Horizontal } from './horizontal';
7
- import { Section } from './section';
8
- import { Vertical } from './vertical';
9
-
10
- const useStyles = makeStyles({
11
- list: {
12
- rowGap: 0
13
- },
14
- listItem: {
15
- padding: tokens.spacingVerticalS,
16
- ':hover': {
17
- backgroundColor: tokens.colorNeutralBackground1Hover
18
- }
19
- },
20
- listItemSelected: {
21
- backgroundColor: tokens.colorNeutralBackground1Selected
22
- },
23
- media: {
24
- width: '32px',
25
- fontSize: tokens.fontSizeBase600,
26
- display: 'flex',
27
- flexDirection: 'column',
28
- justifyContent: 'center'
29
- },
30
- image: {
31
- width: tokens.lineHeightBase600,
32
- height: tokens.lineHeightBase600,
33
- backgroundPosition: 'center center',
34
- backgroundSize: 'cover',
35
- borderRadius: tokens.borderRadiusCircular,
36
- border: `1px solid ${tokens.colorNeutralStroke1}`
37
- },
38
- listItemBody: {
39
- rowGap: 0,
40
- width: 'calc(100% - 44px)'
41
- },
42
- listItemHeader: mixins.ellipsis,
43
- listItemContent: {
44
- ...mixins.ellipsis,
45
- fontSize: tokens.fontSizeBase200
46
- },
47
- listItemMedia: {
48
- ...mixins.ellipsis,
49
- maxWidth: '20%',
50
- '& svg': {
51
- height: tokens.fontSizeBase300
52
- },
53
- '& button': {
54
- padding: 0,
55
- minWidth: 0,
56
- minHeight: 0,
57
- height: '14px'
58
- }
59
- },
60
- listItemMediaNoTrim: {
61
- overflow: 'visible',
62
- maxWidth: 'fit-content'
63
- },
64
- listItemMultilineContent: {
65
- whiteSpace: 'pre-line'
66
- }
67
- });
68
-
69
- export interface iListItem {
70
- key: string | number;
71
- media?: JSX.Element | string;
72
- header: string;
73
- headerMedia?: JSX.Element | string;
74
- content?: string | JSX.Element | (string | JSX.Element)[];
75
- onClickOnMedia?: boolean;
76
- onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
77
- selected?: boolean;
78
- }
79
- interface IProps {
80
- selectable?: boolean;
81
- items: iListItem[];
82
- showAllHeaderMedia?: boolean;
83
- /** allow multiline content */
84
- multiline?: boolean;
85
- dark?: boolean;
86
- }
87
-
88
- export const ListEx = (props: IProps) => {
89
- const ctx = useKWIZFluentContext();
90
- const cssNames = useStyles();
91
- const isDark = ctx.dark === true || props.dark === true;
92
-
93
- const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
94
- {item.media && <Section css={[cssNames.media]} onClick={(e) => {
95
- if (!item.onClickOnMedia)
96
- e.stopPropagation();//media may have its on onclick
97
- }}>{
98
- isString(item.media)
99
- ? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${isDark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
100
- : item.media
101
- }</Section>}
102
- <Vertical main css={[cssNames.listItemBody]}>
103
- <Horizontal main>
104
- <Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
105
- {item.headerMedia && <Section onClick={(e) => {
106
- e.stopPropagation();//media may have its on onclick
107
- }} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
108
- </Horizontal>
109
- {!isNullOrUndefined(item.content)
110
- ? (Array.isArray(item.content) ? item.content : [item.content]).map((c, idx) => isNullOrUndefined(c) ? undefined : <Section key={idx} css={[cssNames.listItemContent, props.multiline ? cssNames.listItemMultilineContent : undefined]}>{c}</Section>)
111
- : undefined}
112
- </Vertical>
113
- </Horizontal>;
114
-
115
- return (
116
- <Vertical css={[cssNames.list, KnownClassNames.list]}>
117
- {props.items.map(item => listItemElm(item))}
118
- </Vertical>
119
- );
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+ import { LOGO_BLUE_SQUARE, LOGO_WHITE_SQUARE, isNullOrUndefined, isString } from '@kwiz/common';
3
+ import React from 'react';
4
+ import { useKWIZFluentContext } from '../helpers/context-internal';
5
+ import { KnownClassNames, mixins } from '../styles/styles';
6
+ import { Horizontal } from './horizontal';
7
+ import { Section } from './section';
8
+ import { Vertical } from './vertical';
9
+
10
+ const useStyles = makeStyles({
11
+ list: {
12
+ rowGap: 0
13
+ },
14
+ listItem: {
15
+ padding: tokens.spacingVerticalS,
16
+ ':hover': {
17
+ backgroundColor: tokens.colorNeutralBackground1Hover
18
+ }
19
+ },
20
+ listItemSelected: {
21
+ backgroundColor: tokens.colorNeutralBackground1Selected
22
+ },
23
+ media: {
24
+ width: '32px',
25
+ fontSize: tokens.fontSizeBase600,
26
+ display: 'flex',
27
+ flexDirection: 'column',
28
+ justifyContent: 'center'
29
+ },
30
+ image: {
31
+ width: tokens.lineHeightBase600,
32
+ height: tokens.lineHeightBase600,
33
+ backgroundPosition: 'center center',
34
+ backgroundSize: 'cover',
35
+ borderRadius: tokens.borderRadiusCircular,
36
+ border: `1px solid ${tokens.colorNeutralStroke1}`
37
+ },
38
+ listItemBody: {
39
+ rowGap: 0,
40
+ width: 'calc(100% - 44px)'
41
+ },
42
+ listItemHeader: mixins.ellipsis,
43
+ listItemContent: {
44
+ ...mixins.ellipsis,
45
+ fontSize: tokens.fontSizeBase200
46
+ },
47
+ listItemMedia: {
48
+ ...mixins.ellipsis,
49
+ maxWidth: '20%',
50
+ '& svg': {
51
+ height: tokens.fontSizeBase300
52
+ },
53
+ '& button': {
54
+ padding: 0,
55
+ minWidth: 0,
56
+ minHeight: 0,
57
+ height: '14px'
58
+ }
59
+ },
60
+ listItemMediaNoTrim: {
61
+ overflow: 'visible',
62
+ maxWidth: 'fit-content'
63
+ },
64
+ listItemMultilineContent: {
65
+ whiteSpace: 'pre-line'
66
+ }
67
+ });
68
+
69
+ export interface iListItem {
70
+ key: string | number;
71
+ media?: JSX.Element | string;
72
+ header: string;
73
+ headerMedia?: JSX.Element | string;
74
+ content?: string | JSX.Element | (string | JSX.Element)[];
75
+ onClickOnMedia?: boolean;
76
+ onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
77
+ selected?: boolean;
78
+ }
79
+ interface IProps {
80
+ selectable?: boolean;
81
+ items: iListItem[];
82
+ showAllHeaderMedia?: boolean;
83
+ /** allow multiline content */
84
+ multiline?: boolean;
85
+ dark?: boolean;
86
+ }
87
+
88
+ export const ListEx = (props: IProps) => {
89
+ const ctx = useKWIZFluentContext();
90
+ const cssNames = useStyles();
91
+ const isDark = ctx.dark === true || props.dark === true;
92
+
93
+ const listItemElm = (item: iListItem) => <Horizontal key={item.key} css={[cssNames.listItem, item.selected && cssNames.listItemSelected]} onClick={item.onClick}>
94
+ {item.media && <Section css={[cssNames.media]} onClick={(e) => {
95
+ if (!item.onClickOnMedia)
96
+ e.stopPropagation();//media may have its on onclick
97
+ }}>{
98
+ isString(item.media)
99
+ ? <div className={cssNames.image} style={{ backgroundImage: `url('${encodeURI(item.media)}'), url('${isDark ? LOGO_WHITE_SQUARE : LOGO_BLUE_SQUARE}')` }}></div>
100
+ : item.media
101
+ }</Section>}
102
+ <Vertical main css={[cssNames.listItemBody]}>
103
+ <Horizontal main>
104
+ <Section main css={[cssNames.listItemHeader]}>{item.header}</Section>
105
+ {item.headerMedia && <Section onClick={(e) => {
106
+ e.stopPropagation();//media may have its on onclick
107
+ }} css={[cssNames.listItemMedia, props.showAllHeaderMedia && cssNames.listItemMediaNoTrim]}>{item.headerMedia}</Section>}
108
+ </Horizontal>
109
+ {!isNullOrUndefined(item.content)
110
+ ? (Array.isArray(item.content) ? item.content : [item.content]).map((c, idx) => isNullOrUndefined(c) ? undefined : <Section key={idx} css={[cssNames.listItemContent, props.multiline ? cssNames.listItemMultilineContent : undefined]}>{c}</Section>)
111
+ : undefined}
112
+ </Vertical>
113
+ </Horizontal>;
114
+
115
+ return (
116
+ <Vertical css={[cssNames.list, KnownClassNames.list]}>
117
+ {props.items.map(item => listItemElm(item))}
118
+ </Vertical>
119
+ );
120
120
  }
@@ -1,11 +1,11 @@
1
- import { LOGO_ANIM_SMALL } from '@kwiz/common';
2
- import { Centered } from './centered';
3
- import React from 'react';
4
-
5
- interface IProps {
6
- }
7
- export const Loading: React.FunctionComponent<IProps> = (props) => {
8
- return (
9
- <Centered><img src={LOGO_ANIM_SMALL} alt="loading" style={{ width: '15vw' }} /></Centered>
10
- );
1
+ import { LOGO_ANIM_SMALL } from '@kwiz/common';
2
+ import { Centered } from './centered';
3
+ import React from 'react';
4
+
5
+ interface IProps {
6
+ }
7
+ export const Loading: React.FunctionComponent<IProps> = (props) => {
8
+ return (
9
+ <Centered><img src={LOGO_ANIM_SMALL} alt="loading" style={{ width: '15vw' }} /></Centered>
10
+ );
11
11
  }
@@ -1,174 +1,174 @@
1
- import { Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuList, MenuListProps, MenuPopover, menuPopoverClassNames, MenuPopoverProps, MenuProps, MenuTrigger } from '@fluentui/react-components';
2
- import { ChevronLeftRegular, ChevronRightRegular } from '@fluentui/react-icons';
3
- import { IDictionary, isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, isNullOrUndefined, isNumber, isString, isUndefined, jsonClone, stopEvent } from '@kwiz/common';
4
- import React from 'react';
5
- import { useKWIZFluentContext } from '../helpers/context-internal';
6
- import { useStateEX } from '../helpers';
7
- import { ButtonEX, ButtonEXProps } from './button';
8
- import { Horizontal } from './horizontal';
9
- import { Search } from './search';
10
- import { Section } from './section';
11
-
12
- interface iMenuItemEXItem {
13
- type?: "item";
14
- title: string;
15
- onClick: () => void;
16
- disabled?: boolean;
17
- icon?: JSX.Element;
18
- items?: iMenuItemEX[];
19
- checked?: boolean;
20
- }
21
- interface iMenuItemEXSeparator {
22
- type: "separator";
23
- }
24
- interface iMenuItemEXGroup {
25
- type: "group";
26
- title: string;
27
- items: iMenuItemEX[];
28
- }
29
- export type iMenuItemEX = iMenuItemEXItem | iMenuItemEXSeparator | iMenuItemEXGroup;
30
-
31
- interface IProps {
32
- menuProps?: MenuProps;
33
- menuPopOverProps?: MenuPopoverProps;
34
- menuListProps?: MenuListProps;
35
- trigger: JSX.Element | string | ButtonEXProps;
36
- items: iMenuItemEX[];
37
- /** default 8 null/false to disable */
38
- filterThreshold?: number | false;
39
- /** default 8, null/false to disable */
40
- pageSize?: number | false;
41
- }
42
- export const MenuEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
43
- const ctx = useKWIZFluentContext();
44
- const [startIndexPerLevel, setStartIndexPerLevel] = useStateEX<IDictionary<number>>({});
45
- const [filterPerLevel, setFilterPerLevel] = useStateEX<IDictionary<string>>({});
46
- let pageSize: number = isUndefined(props.pageSize) ? 8 : isNumber(props.pageSize) ? props.pageSize : 99999999999;
47
- let filterThreshold: number = isUndefined(props.filterThreshold) ? 8 : isNumber(props.filterThreshold) ? props.filterThreshold : 99999999999;
48
-
49
- //when hovering over sub menu the parent would close - have menu trigger keep open on the parent level
50
- const [keepOpen, setKeepOpen] = useStateEX<IDictionary<boolean>>({});
51
- const [opened, setOpened] = useStateEX<IDictionary<boolean>>({});
52
-
53
- React.useEffect(() => {
54
- window.setTimeout(() => {
55
- var menus = document.querySelectorAll(`.${menuPopoverClassNames.root}`);
56
- menus.forEach((menu: HTMLDivElement) => {
57
- var rect = menu.getBoundingClientRect();
58
- if (rect.bottom > document.documentElement.clientHeight) {
59
- menu.style.overflow = "auto";
60
- menu.style.height = `${rect.height - (rect.bottom - document.documentElement.clientHeight)}px`;
61
- }
62
- });
63
- }, 100);
64
- }, [opened]);
65
-
66
- function renderItems(items: iMenuItemEX[], level: number) {
67
- const myLevelFilter = filterPerLevel[level];
68
- //get rid of empty/null items
69
- items = items.filter(i => !isNullOrUndefined(i) && (isNotEmptyString(i.type) || isNotEmptyString((i as iMenuItemEXItem).title)))
70
- if (isNotEmptyString(myLevelFilter)) {
71
- items = items.filter(i => i.type !== "separator" && i.title.toLowerCase().indexOf(myLevelFilter) >= 0);
72
- }
73
-
74
- let menuItems = items.map((item, index) => {
75
- switch (item.type) {
76
- case "group":
77
- return <MenuGroup key={index}>
78
- <MenuGroupHeader>{item.title}</MenuGroupHeader>
79
- {renderItems(item.items, level + 1)}
80
- </MenuGroup>;
81
- case "separator":
82
- return <MenuDivider key={index} />;
83
- case "item":
84
- default:
85
- const openKey = `${level}|${index}`;
86
- const menuItem = <MenuItem key={index} icon={item.icon}
87
- disabled={item.disabled}
88
- onClick={item.onClick}
89
- >{item.title}</MenuItem>;
90
- return isNotEmptyArray(item.items)
91
- ? <Menu key={index} mountNode={ctx.mountNode} open={opened[openKey] || false} onOpenChange={(e, data) => {
92
- if (data.open) {
93
- setOpened({ ...opened, [openKey]: true });
94
- setKeepOpen({ ...keepOpen, [level]: true });
95
- }
96
- else if (!keepOpen[openKey]) {
97
- setOpened({ ...opened, [openKey]: false });
98
- setKeepOpen({ ...keepOpen, [level]: false });
99
- }
100
- }}>
101
- <MenuTrigger disableButtonEnhancement>
102
- {menuItem}
103
- </MenuTrigger>
104
- <MenuPopover>
105
- <MenuList>
106
- {renderItems(item.items, level + 1)}
107
- </MenuList>
108
- </MenuPopover>
109
- </Menu>
110
- : menuItem;
111
- }
112
- });
113
-
114
- const paged = menuItems.length > pageSize;
115
- const filtered = menuItems.length > filterThreshold || !isNullOrEmptyString(myLevelFilter);
116
- const filterControl = filtered && <Search value={myLevelFilter || ""} onChangeDeferred={(newValue) => {
117
- const s = jsonClone(filterPerLevel);
118
- s[level] = newValue ? newValue.toLowerCase() : "";
119
- setFilterPerLevel(s);
120
- }} />;
121
- if (paged) {
122
- let start = startIndexPerLevel[level];
123
- if (isNullOrUndefined(start)) start = 0;
124
- let hasMore = menuItems.length > start + pageSize;
125
- menuItems = menuItems.slice(start, start + pageSize);
126
- if (start > 0 || hasMore) menuItems.splice(0, 0, <Horizontal key='$next'>
127
- <ButtonEX disabled={start < 1} icon={<ChevronLeftRegular />} title='previous' onClick={() => {
128
- const s = jsonClone(startIndexPerLevel);
129
- s[level] = start - pageSize;
130
- setStartIndexPerLevel(s);
131
- }} />
132
- <Section main>
133
- {filterControl}
134
- </Section>
135
- <ButtonEX disabled={!hasMore} icon={<ChevronRightRegular />} title='next' onClick={() => {
136
- const s = jsonClone(startIndexPerLevel);
137
- s[level] = start + pageSize;
138
- setStartIndexPerLevel(s);
139
- }} />
140
- </Horizontal>);
141
- }
142
- else if (filtered) {
143
- //just filter - no paging
144
- menuItems.splice(0, 0, <Horizontal key='$next'>
145
- {filterControl}
146
- </Horizontal>);
147
- }
148
- return menuItems;
149
- }
150
-
151
- return (
152
- <Menu mountNode={ctx.mountNode} {...props.menuProps} open={opened[0] || false} onOpenChange={(e, data) => {
153
- if (data.open) setOpened({ ...opened, 0: true });
154
- else if (!keepOpen[0]) setOpened({ ...opened, 0: false });
155
- }}>
156
- <MenuTrigger disableButtonEnhancement>
157
- {isString(props.trigger)
158
- ? <ButtonEX title={props.trigger} onClick={(e) => {
159
- stopEvent(e);
160
- }} />
161
- : isString((props.trigger as ButtonEXProps).title)
162
- ? <ButtonEX {...(props.trigger as ButtonEXProps)} onClick={(e) => {
163
- stopEvent(e);
164
- }} />
165
- : props.trigger as JSX.Element}
166
- </MenuTrigger>
167
- <MenuPopover {...props.menuPopOverProps}>
168
- <MenuList {...props.menuListProps}>
169
- {renderItems(props.items, 0)}
170
- </MenuList>
171
- </MenuPopover>
172
- </Menu>
173
- );
1
+ import { Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuList, MenuListProps, MenuPopover, menuPopoverClassNames, MenuPopoverProps, MenuProps, MenuTrigger } from '@fluentui/react-components';
2
+ import { ChevronLeftRegular, ChevronRightRegular } from '@fluentui/react-icons';
3
+ import { IDictionary, isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, isNullOrUndefined, isNumber, isString, isUndefined, jsonClone, stopEvent } from '@kwiz/common';
4
+ import React from 'react';
5
+ import { useStateEX } from '../helpers';
6
+ import { useKWIZFluentContext } from '../helpers/context-internal';
7
+ import { ButtonEX, ButtonEXProps } from './button';
8
+ import { Horizontal } from './horizontal';
9
+ import { Search } from './search';
10
+ import { Section } from './section';
11
+
12
+ interface iMenuItemEXItem {
13
+ type?: "item";
14
+ title: string;
15
+ onClick: () => void;
16
+ disabled?: boolean;
17
+ icon?: JSX.Element;
18
+ items?: iMenuItemEX[];
19
+ checked?: boolean;
20
+ }
21
+ interface iMenuItemEXSeparator {
22
+ type: "separator";
23
+ }
24
+ interface iMenuItemEXGroup {
25
+ type: "group";
26
+ title: string;
27
+ items: iMenuItemEX[];
28
+ }
29
+ export type iMenuItemEX = iMenuItemEXItem | iMenuItemEXSeparator | iMenuItemEXGroup;
30
+
31
+ interface IProps {
32
+ menuProps?: MenuProps;
33
+ menuPopOverProps?: MenuPopoverProps;
34
+ menuListProps?: MenuListProps;
35
+ trigger: JSX.Element | string | ButtonEXProps;
36
+ items: iMenuItemEX[];
37
+ /** default 8 null/false to disable */
38
+ filterThreshold?: number | false;
39
+ /** default 8, null/false to disable */
40
+ pageSize?: number | false;
41
+ }
42
+ export const MenuEx: React.FunctionComponent<React.PropsWithChildren<IProps>> = (props) => {
43
+ const ctx = useKWIZFluentContext();
44
+ const [startIndexPerLevel, setStartIndexPerLevel] = useStateEX<IDictionary<number>>({});
45
+ const [filterPerLevel, setFilterPerLevel] = useStateEX<IDictionary<string>>({});
46
+ let pageSize: number = isUndefined(props.pageSize) ? 8 : isNumber(props.pageSize) ? props.pageSize : 99999999999;
47
+ let filterThreshold: number = isUndefined(props.filterThreshold) ? 8 : isNumber(props.filterThreshold) ? props.filterThreshold : 99999999999;
48
+
49
+ //when hovering over sub menu the parent would close - have menu trigger keep open on the parent level
50
+ const [keepOpen, setKeepOpen] = useStateEX<IDictionary<boolean>>({});
51
+ const [opened, setOpened] = useStateEX<IDictionary<boolean>>({});
52
+
53
+ React.useEffect(() => {
54
+ window.setTimeout(() => {
55
+ var menus = document.querySelectorAll(`.${menuPopoverClassNames.root}`);
56
+ menus.forEach((menu: HTMLDivElement) => {
57
+ var rect = menu.getBoundingClientRect();
58
+ if (rect.bottom > document.documentElement.clientHeight) {
59
+ menu.style.overflow = "auto";
60
+ menu.style.height = `${rect.height - (rect.bottom - document.documentElement.clientHeight)}px`;
61
+ }
62
+ });
63
+ }, 100);
64
+ }, [opened]);
65
+
66
+ function renderItems(items: iMenuItemEX[], level: number) {
67
+ const myLevelFilter = filterPerLevel[level];
68
+ //get rid of empty/null items
69
+ items = items.filter(i => !isNullOrUndefined(i) && (isNotEmptyString(i.type) || isNotEmptyString((i as iMenuItemEXItem).title)))
70
+ if (isNotEmptyString(myLevelFilter)) {
71
+ items = items.filter(i => i.type !== "separator" && i.title.toLowerCase().indexOf(myLevelFilter) >= 0);
72
+ }
73
+
74
+ let menuItems = items.map((item, index) => {
75
+ switch (item.type) {
76
+ case "group":
77
+ return <MenuGroup key={index}>
78
+ <MenuGroupHeader>{item.title}</MenuGroupHeader>
79
+ {renderItems(item.items, level + 1)}
80
+ </MenuGroup>;
81
+ case "separator":
82
+ return <MenuDivider key={index} />;
83
+ case "item":
84
+ default:
85
+ const openKey = `${level}|${index}`;
86
+ const menuItem = <MenuItem key={index} icon={item.icon}
87
+ disabled={item.disabled}
88
+ onClick={item.onClick}
89
+ >{item.title}</MenuItem>;
90
+ return isNotEmptyArray(item.items)
91
+ ? <Menu key={index} mountNode={ctx.mountNode} open={opened[openKey] || false} onOpenChange={(e, data) => {
92
+ if (data.open) {
93
+ setOpened({ ...opened, [openKey]: true });
94
+ setKeepOpen({ ...keepOpen, [level]: true });
95
+ }
96
+ else if (!keepOpen[openKey]) {
97
+ setOpened({ ...opened, [openKey]: false });
98
+ setKeepOpen({ ...keepOpen, [level]: false });
99
+ }
100
+ }}>
101
+ <MenuTrigger disableButtonEnhancement>
102
+ {menuItem}
103
+ </MenuTrigger>
104
+ <MenuPopover>
105
+ <MenuList>
106
+ {renderItems(item.items, level + 1)}
107
+ </MenuList>
108
+ </MenuPopover>
109
+ </Menu>
110
+ : menuItem;
111
+ }
112
+ });
113
+
114
+ const paged = menuItems.length > pageSize;
115
+ const filtered = menuItems.length > filterThreshold || !isNullOrEmptyString(myLevelFilter);
116
+ const filterControl = filtered && <Search defaultValue={myLevelFilter || ""} onChangeDeferred={(newValue) => {
117
+ const s = jsonClone(filterPerLevel);
118
+ s[level] = newValue ? newValue.toLowerCase() : "";
119
+ setFilterPerLevel(s);
120
+ }} />;
121
+ if (paged) {
122
+ let start = startIndexPerLevel[level];
123
+ if (isNullOrUndefined(start)) start = 0;
124
+ let hasMore = menuItems.length > start + pageSize;
125
+ menuItems = menuItems.slice(start, start + pageSize);
126
+ if (start > 0 || hasMore) menuItems.splice(0, 0, <Horizontal key='$next'>
127
+ <ButtonEX disabled={start < 1} icon={<ChevronLeftRegular />} title='previous' onClick={() => {
128
+ const s = jsonClone(startIndexPerLevel);
129
+ s[level] = start - pageSize;
130
+ setStartIndexPerLevel(s);
131
+ }} />
132
+ <Section main>
133
+ {filterControl}
134
+ </Section>
135
+ <ButtonEX disabled={!hasMore} icon={<ChevronRightRegular />} title='next' onClick={() => {
136
+ const s = jsonClone(startIndexPerLevel);
137
+ s[level] = start + pageSize;
138
+ setStartIndexPerLevel(s);
139
+ }} />
140
+ </Horizontal>);
141
+ }
142
+ else if (filtered) {
143
+ //just filter - no paging
144
+ menuItems.splice(0, 0, <Horizontal key='$next'>
145
+ {filterControl}
146
+ </Horizontal>);
147
+ }
148
+ return menuItems;
149
+ }
150
+
151
+ return (
152
+ <Menu mountNode={ctx.mountNode} {...props.menuProps} open={opened[0] || false} onOpenChange={(e, data) => {
153
+ if (data.open) setOpened({ ...opened, 0: true });
154
+ else if (!keepOpen[0]) setOpened({ ...opened, 0: false });
155
+ }}>
156
+ <MenuTrigger disableButtonEnhancement>
157
+ {isString(props.trigger)
158
+ ? <ButtonEX title={props.trigger} onClick={(e) => {
159
+ stopEvent(e);
160
+ }} />
161
+ : isString((props.trigger as ButtonEXProps).title)
162
+ ? <ButtonEX {...(props.trigger as ButtonEXProps)} onClick={(e) => {
163
+ stopEvent(e);
164
+ }} />
165
+ : props.trigger as JSX.Element}
166
+ </MenuTrigger>
167
+ <MenuPopover {...props.menuPopOverProps}>
168
+ <MenuList {...props.menuListProps}>
169
+ {renderItems(props.items, 0)}
170
+ </MenuList>
171
+ </MenuPopover>
172
+ </Menu>
173
+ );
174
174
  }