@reykjavik/hanna-react 0.10.111 → 0.10.113
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.
- package/Alert.js +1 -1
- package/BasicTable.js +4 -4
- package/CHANGELOG.md +34 -1
- package/ContactBubble.js +8 -3
- package/Datepicker.d.ts +1 -0
- package/Datepicker.js +51 -13
- package/{_abstract/_FocusTrap.d.ts → FocusTrap.d.ts} +5 -1
- package/{_abstract/_FocusTrap.js → FocusTrap.js} +5 -1
- package/Layout.d.ts +7 -3
- package/Layout.js +7 -26
- package/MainMenu.d.ts +52 -20
- package/MainMenu.js +27 -6
- package/MainMenu2.d.ts +114 -0
- package/MainMenu2.js +235 -0
- package/MobileMenuToggler/_useMobileMenuToggling.d.ts +21 -0
- package/{utils/useMenuToggling.js → MobileMenuToggler/_useMobileMenuToggling.js} +34 -14
- package/MobileMenuToggler.d.ts +21 -0
- package/MobileMenuToggler.js +43 -0
- package/Multiselect.js +13 -6
- package/TagPill.d.ts +2 -0
- package/_abstract/_AbstractModal.js +9 -4
- package/_abstract/_Button.d.ts +3 -3
- package/_abstract/_Button.js +3 -3
- package/_abstract/_Table.d.ts +3 -3
- package/_abstract/_Table.js +1 -1
- package/esm/Alert.js +1 -1
- package/esm/BasicTable.js +4 -4
- package/esm/ContactBubble.js +8 -3
- package/esm/Datepicker.d.ts +1 -0
- package/esm/Datepicker.js +51 -13
- package/esm/{_abstract/_FocusTrap.d.ts → FocusTrap.d.ts} +5 -1
- package/esm/{_abstract/_FocusTrap.js → FocusTrap.js} +5 -1
- package/esm/Layout.d.ts +7 -3
- package/esm/Layout.js +8 -27
- package/esm/MainMenu.d.ts +52 -20
- package/esm/MainMenu.js +27 -7
- package/esm/MainMenu2.d.ts +114 -0
- package/esm/MainMenu2.js +230 -0
- package/esm/MobileMenuToggler/_useMobileMenuToggling.d.ts +21 -0
- package/esm/{utils/useMenuToggling.js → MobileMenuToggler/_useMobileMenuToggling.js} +32 -13
- package/esm/MobileMenuToggler.d.ts +21 -0
- package/esm/MobileMenuToggler.js +37 -0
- package/esm/Multiselect.js +12 -5
- package/esm/TagPill.d.ts +2 -0
- package/esm/_abstract/_AbstractModal.js +7 -2
- package/esm/_abstract/_Button.d.ts +3 -3
- package/esm/_abstract/_Button.js +3 -3
- package/esm/_abstract/_Table.d.ts +3 -3
- package/esm/_abstract/_Table.js +1 -1
- package/esm/index.d.ts +3 -0
- package/esm/utils/a11yHelpers.d.ts +2 -0
- package/esm/utils/a11yHelpers.js +11 -0
- package/esm/utils/types.d.ts +4 -0
- package/esm/utils/useFormatMonitor.d.ts +4 -11
- package/esm/utils/useFormatMonitor.js +0 -10
- package/esm/utils.d.ts +7 -2
- package/esm/utils.js +8 -2
- package/index.d.ts +3 -0
- package/package.json +13 -1
- package/utils/a11yHelpers.d.ts +2 -0
- package/utils/a11yHelpers.js +15 -0
- package/utils/types.d.ts +4 -0
- package/utils/useFormatMonitor.d.ts +4 -11
- package/utils/useFormatMonitor.js +0 -10
- package/utils.d.ts +7 -2
- package/utils.js +9 -5
- package/esm/utils/HannaUIState.d.ts +0 -7
- package/esm/utils/HannaUIState.js +0 -7
- package/esm/utils/useMenuToggling.d.ts +0 -10
- package/utils/HannaUIState.d.ts +0 -7
- package/utils/HannaUIState.js +0 -11
- package/utils/useMenuToggling.d.ts +0 -10
package/esm/MainMenu2.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { modifiedClass } from '@hugsmidjan/qj/classUtils';
|
|
3
|
+
import { getIllustrationUrl } from '@reykjavik/hanna-utils/assets';
|
|
4
|
+
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
5
|
+
import { Link } from './_abstract/_Link.js';
|
|
6
|
+
import { handleAnchorLinkClick } from './utils/a11yHelpers.js';
|
|
7
|
+
import ButtonPrimary from './ButtonPrimary.js';
|
|
8
|
+
import ButtonSecondary from './ButtonSecondary.js';
|
|
9
|
+
import { FocusTrap } from './FocusTrap.js';
|
|
10
|
+
import { useDomid, useIsBrowserSide, } from './utils.js';
|
|
11
|
+
const htmlCl =
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
13
|
+
(typeof document !== 'undefined') && document.documentElement.classList;
|
|
14
|
+
const globalClasses = {
|
|
15
|
+
// menuIsActive: '.menu-is-active',
|
|
16
|
+
menuIsOpen: 'menu-is-open',
|
|
17
|
+
menuIsClosed: 'menu-is-closed',
|
|
18
|
+
};
|
|
19
|
+
export const defaultMainMenu2Texts = {
|
|
20
|
+
is: {
|
|
21
|
+
title: 'Aðalvalmynd',
|
|
22
|
+
homeLink: 'Forsíða',
|
|
23
|
+
openMenu: 'Valmynd',
|
|
24
|
+
openMenuLong: 'Opna Aðalvalmynd',
|
|
25
|
+
closeMenu: 'Loka',
|
|
26
|
+
closeMenuLong: 'Loka Aðalvalmynd',
|
|
27
|
+
},
|
|
28
|
+
en: {
|
|
29
|
+
title: 'Main Menu',
|
|
30
|
+
homeLink: 'Home page',
|
|
31
|
+
openMenu: 'Menu',
|
|
32
|
+
openMenuLong: 'Open main menu',
|
|
33
|
+
closeMenu: 'Close',
|
|
34
|
+
closeMenuLong: 'Close main menu',
|
|
35
|
+
},
|
|
36
|
+
pl: {
|
|
37
|
+
title: 'Menu główne',
|
|
38
|
+
homeLink: 'Strona główna',
|
|
39
|
+
openMenu: 'Menu',
|
|
40
|
+
openMenuLong: 'Otwórz menu główne',
|
|
41
|
+
closeMenu: 'Zamknij',
|
|
42
|
+
closeMenuLong: 'Zamknij menu główne',
|
|
43
|
+
},
|
|
44
|
+
se: {
|
|
45
|
+
title: 'Huvudmeny',
|
|
46
|
+
homeLink: 'Förstasida',
|
|
47
|
+
openMenu: 'Meny',
|
|
48
|
+
openMenuLong: 'Öppna huvudmenyn',
|
|
49
|
+
closeMenu: 'Stäng',
|
|
50
|
+
closeMenuLong: 'Stäng huvudmenyn',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const iconMap = {
|
|
55
|
+
alert: 'info',
|
|
56
|
+
globe: undefined,
|
|
57
|
+
search: 'search',
|
|
58
|
+
user: 'user',
|
|
59
|
+
// NOTE: We're temporarily coerceing `IconName` to `ButtonIcon`
|
|
60
|
+
// TODO: Remove this once Hanna icons (and `ButtonIcons` sperifically)
|
|
61
|
+
// have been expanded better standardised.
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Function that turns menu item props/objects into HTML
|
|
65
|
+
* rendering <a/> or <button/> elements depending on the context,
|
|
66
|
+
* Whether we're rendering the menu on the server or in the browser, etc.
|
|
67
|
+
*/
|
|
68
|
+
const getRenderers = (props) => {
|
|
69
|
+
const { onItemClick, closeMenu, isBrowser } = props;
|
|
70
|
+
// eslint-disable-next-line complexity
|
|
71
|
+
const renderItem = (classPrefix, item, opts = {}) => {
|
|
72
|
+
const { key, Tag = 'li', button } = opts;
|
|
73
|
+
if (typeof item === 'function') {
|
|
74
|
+
const Item = item;
|
|
75
|
+
return (React.createElement("li", { key: key, className: `${classPrefix}item` },
|
|
76
|
+
React.createElement(Item, { closeMenu: closeMenu })));
|
|
77
|
+
}
|
|
78
|
+
const linkClassName = `${classPrefix}link`;
|
|
79
|
+
const { label, labelLong, href, target, lang, controlsId, onClick, descr, icon } = item;
|
|
80
|
+
const itemDescr = descr && (React.createElement(React.Fragment, null,
|
|
81
|
+
' ',
|
|
82
|
+
React.createElement("small", { className: `${linkClassName}__descr` }, descr)));
|
|
83
|
+
const ButtonTag = button ? ButtonSecondary : 'button';
|
|
84
|
+
const LinkTag = button ? ButtonSecondary : Link;
|
|
85
|
+
const buttonCompProps = button
|
|
86
|
+
? {
|
|
87
|
+
size: 'small',
|
|
88
|
+
'data-icon': icon && iconMap[icon],
|
|
89
|
+
}
|
|
90
|
+
: undefined;
|
|
91
|
+
return (React.createElement(Tag, { key: key, className: modifiedClass(`${classPrefix}item`, item.modifier), "aria-current": item.current || undefined }, isBrowser && (onClick || !href) ? (React.createElement(ButtonTag, Object.assign({ className: linkClassName, type: "button", onClick: () => {
|
|
92
|
+
const keepOpen1 = onClick && onClick(item) === false;
|
|
93
|
+
const keepOpen2 = onItemClick && onItemClick(item) === false;
|
|
94
|
+
!(keepOpen1 || keepOpen2) && closeMenu();
|
|
95
|
+
}, "aria-controls": controlsId, "aria-label": labelLong, title: labelLong, lang: lang }, buttonCompProps),
|
|
96
|
+
label,
|
|
97
|
+
" ",
|
|
98
|
+
itemDescr)) : href ? (React.createElement(LinkTag, Object.assign({ className: linkClassName, href: href, target: target, "aria-label": labelLong, title: labelLong, onClick: () => {
|
|
99
|
+
const keepOpen = onItemClick && onItemClick(item) === false;
|
|
100
|
+
!keepOpen && closeMenu();
|
|
101
|
+
}, lang: lang, hrefLang: item.hrefLang }, buttonCompProps),
|
|
102
|
+
label,
|
|
103
|
+
" ",
|
|
104
|
+
itemDescr)) : null));
|
|
105
|
+
};
|
|
106
|
+
const renderList = (classSuffix, items, opts = {}) => {
|
|
107
|
+
if (!items || !items.length) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return (React.createElement("ul", Object.assign({ className: `${classSuffix}items` }, opts.listProps), items.map((listItem, i) => renderItem(classSuffix, listItem, { key: i, button: opts.buttons }))));
|
|
111
|
+
};
|
|
112
|
+
return { renderList, renderItem };
|
|
113
|
+
};
|
|
114
|
+
// eslint-disable-next-line complexity
|
|
115
|
+
export const MainMenu2 = (props) => {
|
|
116
|
+
const { homeLink = '/', items, onItemClick, illustration, imageUrl, wrapperProps = {}, } = props;
|
|
117
|
+
const domid = useDomid(wrapperProps.id);
|
|
118
|
+
const isBrowser = useIsBrowserSide(props.ssr);
|
|
119
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
120
|
+
const _wrapperRef = useRef(null);
|
|
121
|
+
const wrapperRef = wrapperProps.ref || _wrapperRef;
|
|
122
|
+
const escHandler = useCallback(
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
124
|
+
(e) => e.key === 'Escape' && closeMenu(),
|
|
125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
+
[]);
|
|
127
|
+
const openMenu = () => {
|
|
128
|
+
htmlCl.add(globalClasses.menuIsOpen);
|
|
129
|
+
htmlCl.remove(globalClasses.menuIsClosed);
|
|
130
|
+
setIsMenuOpen(true);
|
|
131
|
+
document.addEventListener('keydown', escHandler);
|
|
132
|
+
};
|
|
133
|
+
const closeMenu = () => {
|
|
134
|
+
htmlCl.remove(globalClasses.menuIsOpen);
|
|
135
|
+
htmlCl.add(globalClasses.menuIsClosed);
|
|
136
|
+
setIsMenuOpen(false);
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
138
|
+
setActiveSubmenu(defaultActive);
|
|
139
|
+
wrapperRef.current.scrollTo(0, 0);
|
|
140
|
+
document.removeEventListener('keydown', escHandler);
|
|
141
|
+
};
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!isBrowser) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
htmlCl.add(globalClasses.menuIsClosed);
|
|
147
|
+
return () => {
|
|
148
|
+
closeMenu();
|
|
149
|
+
htmlCl.remove(globalClasses.menuIsClosed);
|
|
150
|
+
};
|
|
151
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
152
|
+
}, [isBrowser]);
|
|
153
|
+
const { mainItems, defaultActive } = useMemo(() => {
|
|
154
|
+
if (!items.main || !items.main.length) {
|
|
155
|
+
return { mainItems: undefined, defaultActive: -1 };
|
|
156
|
+
}
|
|
157
|
+
const mainItems = items.main.map((item) => {
|
|
158
|
+
var _a;
|
|
159
|
+
if (!('title' in item) || 'current' in item) {
|
|
160
|
+
return item;
|
|
161
|
+
}
|
|
162
|
+
const current = (_a = item.subItems.find((subItem) => 'current' in subItem && !!subItem.current)) === null || _a === void 0 ? void 0 : _a.current;
|
|
163
|
+
return Object.assign(Object.assign({}, item), { current });
|
|
164
|
+
});
|
|
165
|
+
const defaultActive = mainItems.findIndex((item) => 'current' in item && item.current);
|
|
166
|
+
return { mainItems, defaultActive };
|
|
167
|
+
}, [items.main]);
|
|
168
|
+
const [activeSubmenu, setActiveSubmenu] = useState(defaultActive);
|
|
169
|
+
// Insta-reset activeSubmenu when defaultActive changes (i.e. when the
|
|
170
|
+
// menu-items updated) because we otherwise retain the menu's
|
|
171
|
+
// activeSubmenu state between open/close cycles.
|
|
172
|
+
const lastDefaultActive = useRef(defaultActive);
|
|
173
|
+
if (defaultActive !== lastDefaultActive.current) {
|
|
174
|
+
lastDefaultActive.current = defaultActive;
|
|
175
|
+
setActiveSubmenu(defaultActive);
|
|
176
|
+
}
|
|
177
|
+
const txt = getTexts(props, defaultMainMenu2Texts);
|
|
178
|
+
const { renderItem, renderList } = getRenderers({
|
|
179
|
+
onItemClick,
|
|
180
|
+
closeMenu,
|
|
181
|
+
isBrowser,
|
|
182
|
+
});
|
|
183
|
+
const homeLinkItem = Object.assign(Object.assign({}, (typeof homeLink === 'string'
|
|
184
|
+
? { href: homeLink, label: txt.homeLink }
|
|
185
|
+
: homeLink)), { modifier: 'home' });
|
|
186
|
+
const menuImageUrl = imageUrl || (illustration && getIllustrationUrl(illustration));
|
|
187
|
+
const menuId = `${domid}-menu`;
|
|
188
|
+
return (React.createElement("nav", Object.assign({}, props.wrapperProps, { className: modifiedClass('MainMenu2', isBrowser && (isMenuOpen ? 'open' : 'closed'), wrapperProps.className), style: menuImageUrl
|
|
189
|
+
? Object.assign(Object.assign({}, wrapperProps.style), { '--menu-image': `url(${menuImageUrl})` })
|
|
190
|
+
: wrapperProps.style, ref: wrapperRef, "aria-label": txt.title, "data-sprinkled": isBrowser, id: menuId }),
|
|
191
|
+
isMenuOpen && React.createElement(FocusTrap, { atTop: true }),
|
|
192
|
+
React.createElement("div", { className: "MainMenu2__content" },
|
|
193
|
+
React.createElement("h2", { className: "MainMenu2__title" }, txt.title),
|
|
194
|
+
isBrowser ? (React.createElement(ButtonPrimary, Object.assign({ className: "MainMenu2__toggler", size: "small", type: "button", "aria-pressed": isMenuOpen, "aria-controls": menuId, "data-icon": "text" }, (isMenuOpen
|
|
195
|
+
? {
|
|
196
|
+
onClick: closeMenu,
|
|
197
|
+
'aria-label': txt.closeMenuLong,
|
|
198
|
+
title: txt.closeMenuLong,
|
|
199
|
+
children: txt.closeMenu,
|
|
200
|
+
}
|
|
201
|
+
: {
|
|
202
|
+
onClick: openMenu,
|
|
203
|
+
'aria-label': txt.openMenuLong,
|
|
204
|
+
title: txt.openMenuLong,
|
|
205
|
+
children: txt.openMenu,
|
|
206
|
+
})))) : (React.createElement(ButtonPrimary, { className: "MainMenu2__toggler", size: "small", href: `#${menuId}`, onClick: handleAnchorLinkClick, "aria-hidden": "true", "data-icon": "text" }, txt.title)),
|
|
207
|
+
mainItems && (React.createElement("div", { className: modifiedClass('MainMenu2__main', activeSubmenu < 0 && 'noneActive') },
|
|
208
|
+
renderItem('MainMenu2__main__', homeLinkItem, { Tag: 'div' }),
|
|
209
|
+
mainItems.map((mainItem, i) => {
|
|
210
|
+
if ('title' in mainItem) {
|
|
211
|
+
const submenuId = `${domid}-submenu-${i}`;
|
|
212
|
+
const isActive = i === activeSubmenu;
|
|
213
|
+
return (React.createElement(Fragment, { key: i },
|
|
214
|
+
React.createElement("div", { className: "MainMenu2__main__item", "aria-current": mainItem.current || undefined }, isBrowser ? (React.createElement("button", { className: "MainMenu2__main__link", type: "button", onClick: () => setActiveSubmenu(i), "aria-controls": submenuId, "aria-pressed": isActive }, mainItem.title)) : (React.createElement("strong", { className: "MainMenu2__main__link" }, mainItem.title))),
|
|
215
|
+
renderList('MainMenu2__main__sub__', mainItem.subItems, isBrowser && {
|
|
216
|
+
listProps: {
|
|
217
|
+
id: submenuId,
|
|
218
|
+
hidden: !isActive,
|
|
219
|
+
},
|
|
220
|
+
})));
|
|
221
|
+
}
|
|
222
|
+
return renderItem('MainMenu2__main__', mainItem, { key: i, Tag: 'div' });
|
|
223
|
+
}))),
|
|
224
|
+
renderList('MainMenu2__hot__', items.hot, { buttons: true }),
|
|
225
|
+
renderList('MainMenu2__extra__', items.extra, { buttons: true }),
|
|
226
|
+
items.related && items.related.length > 0 && (React.createElement("div", { className: "MainMenu2__related" },
|
|
227
|
+
items.relatedTitle && (React.createElement("h3", { className: "MainMenu2__related__title" }, items.relatedTitle)),
|
|
228
|
+
renderList('MainMenu2__related__', items.related)))),
|
|
229
|
+
isMenuOpen && React.createElement(FocusTrap, null)));
|
|
230
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
type MenuTogglingState = {
|
|
3
|
+
isMenuActive: true | undefined;
|
|
4
|
+
isMenuOpen: boolean;
|
|
5
|
+
toggleMenu: () => void;
|
|
6
|
+
closeMenu: () => void;
|
|
7
|
+
uiState: MobileMenuTogglerState;
|
|
8
|
+
};
|
|
9
|
+
type Opts = {
|
|
10
|
+
doInitialize?: boolean;
|
|
11
|
+
togglerElm?: string | HTMLElement;
|
|
12
|
+
};
|
|
13
|
+
export declare const useMobileMenuToggling: (opts?: boolean | Opts) => MenuTogglingState;
|
|
14
|
+
export type MobileMenuTogglerState = {
|
|
15
|
+
closeHamburgerMenu: () => void;
|
|
16
|
+
isHamburgerMenuOpen: boolean | undefined;
|
|
17
|
+
isHamburgerMenuActive: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare const MobileMenuStateProvider: import("react").Provider<MobileMenuTogglerState>;
|
|
20
|
+
export declare const useMobileMenuTogglerState: () => MobileMenuTogglerState;
|
|
21
|
+
export {};
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { useRef, useState } from 'react';
|
|
1
|
+
import { createContext, useContext, useRef, useState } from 'react';
|
|
2
2
|
import { focusElement } from '@reykjavik/hanna-utils';
|
|
3
|
-
import { useFormatMonitor } from '
|
|
3
|
+
import { useFormatMonitor } from '../utils/useFormatMonitor.js';
|
|
4
4
|
const htmlClass = (className, add) => {
|
|
5
5
|
document.documentElement.classList[add ? 'add' : 'remove'](className);
|
|
6
6
|
};
|
|
7
7
|
const noop = () => undefined;
|
|
8
|
-
|
|
9
|
-
export const
|
|
8
|
+
const HamburgerMedias = { phone: 1, phablet: 1, tablet: 1 };
|
|
9
|
+
export const useMobileMenuToggling = (opts) => {
|
|
10
|
+
const { doInitialize, togglerElm = '.MainMenuToggler' } = typeof opts === 'boolean'
|
|
11
|
+
? ({ doInitialize: opts })
|
|
12
|
+
: !opts
|
|
13
|
+
? ({ doInitialize: true })
|
|
14
|
+
: opts;
|
|
10
15
|
const stateRef = useRef({
|
|
11
16
|
isMenuOpen: false,
|
|
12
17
|
isMenuActive: undefined,
|
|
@@ -19,16 +24,17 @@ export const useMenuToggling = (doInitialize = true) => {
|
|
|
19
24
|
}
|
|
20
25
|
: noop,
|
|
21
26
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
22
|
-
closeMenu: () =>
|
|
27
|
+
closeMenu: doInitialize ? () => _closeMenu() : noop,
|
|
23
28
|
});
|
|
24
|
-
const [{ isMenuOpen, isMenuActive, uiState }, setMenuState] = useState({
|
|
29
|
+
const [{ isMenuOpen, isMenuActive, uiState }, setMenuState] = useState(() => ({
|
|
25
30
|
isMenuOpen: false,
|
|
26
31
|
isMenuActive: undefined,
|
|
27
32
|
uiState: {
|
|
28
33
|
closeHamburgerMenu: () => stateRef.current.closeMenu(),
|
|
29
34
|
isHamburgerMenuOpen: false,
|
|
35
|
+
isHamburgerMenuActive: false,
|
|
30
36
|
},
|
|
31
|
-
});
|
|
37
|
+
}));
|
|
32
38
|
stateRef.current.isMenuOpen = isMenuOpen;
|
|
33
39
|
stateRef.current.isMenuActive = isMenuActive;
|
|
34
40
|
const _openMenu = () => {
|
|
@@ -36,7 +42,11 @@ export const useMenuToggling = (doInitialize = true) => {
|
|
|
36
42
|
setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuOpen: true, uiState: Object.assign(Object.assign({}, state.uiState), { isHamburgerMenuOpen: true }) })));
|
|
37
43
|
htmlClass('menu-is-open', true);
|
|
38
44
|
htmlClass('menu-is-closed', false);
|
|
39
|
-
|
|
45
|
+
const toggler = typeof togglerElm === 'string' ? document.querySelector(togglerElm) : togglerElm;
|
|
46
|
+
const menuElmId = toggler === null || toggler === void 0 ? void 0 : toggler.getAttribute('aria-controls');
|
|
47
|
+
if (menuElmId) {
|
|
48
|
+
focusElement('#' + menuElmId);
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
};
|
|
42
52
|
const _closeMenu = () => {
|
|
@@ -44,19 +54,21 @@ export const useMenuToggling = (doInitialize = true) => {
|
|
|
44
54
|
setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuOpen: false, uiState: Object.assign(Object.assign({}, state.uiState), { isHamburgerMenuOpen: false }) })));
|
|
45
55
|
htmlClass('menu-is-closed', true);
|
|
46
56
|
htmlClass('menu-is-open', false);
|
|
47
|
-
focusElement(
|
|
57
|
+
focusElement(togglerElm);
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
useFormatMonitor(doInitialize
|
|
51
61
|
? (media) => {
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
const becameHamburger = HamburgerMedias[media.is] && !HamburgerMedias[media.was || ''];
|
|
63
|
+
const leftHamburger = !HamburgerMedias[media.is] && HamburgerMedias[media.was || ''];
|
|
64
|
+
if (becameHamburger) {
|
|
65
|
+
setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuActive: true, uiState: Object.assign(Object.assign({}, state.uiState), { isHamburgerMenuActive: true }) })));
|
|
54
66
|
htmlClass('menu-is-active', true);
|
|
55
67
|
htmlClass('menu-is-closed', true);
|
|
56
68
|
}
|
|
57
|
-
if (
|
|
69
|
+
else if (leftHamburger) {
|
|
58
70
|
_closeMenu();
|
|
59
|
-
setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuActive: undefined })));
|
|
71
|
+
setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuActive: undefined, uiState: Object.assign(Object.assign({}, state.uiState), { isHamburgerMenuActive: false }) })));
|
|
60
72
|
htmlClass('menu-is-active', false);
|
|
61
73
|
htmlClass('menu-is-closed', false);
|
|
62
74
|
}
|
|
@@ -70,3 +82,10 @@ export const useMenuToggling = (doInitialize = true) => {
|
|
|
70
82
|
uiState,
|
|
71
83
|
};
|
|
72
84
|
};
|
|
85
|
+
const _MobileMenuTogglerContext = createContext({
|
|
86
|
+
closeHamburgerMenu: () => undefined,
|
|
87
|
+
isHamburgerMenuOpen: undefined,
|
|
88
|
+
isHamburgerMenuActive: undefined,
|
|
89
|
+
});
|
|
90
|
+
export const MobileMenuStateProvider = _MobileMenuTogglerContext.Provider;
|
|
91
|
+
export const useMobileMenuTogglerState = () => useContext(_MobileMenuTogglerContext);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { DefaultTexts } from '@reykjavik/hanna-utils/i18n';
|
|
3
|
+
import { I18NProps } from './utils/types.js';
|
|
4
|
+
import { SSRSupportProps } from './utils.js';
|
|
5
|
+
export type MobileMenuTogglerI18n = {
|
|
6
|
+
togglerLabel: string;
|
|
7
|
+
closeMenuLabel: string;
|
|
8
|
+
closeMenuLabelLong?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const defaultMobileMenuTogglerTexts: DefaultTexts<MobileMenuTogglerI18n>;
|
|
11
|
+
export type MobileMenuTogglerProps = {
|
|
12
|
+
/** The DOM id of the menu that is being toggled */
|
|
13
|
+
controlsId: string;
|
|
14
|
+
children: NonNullable<ReactNode>;
|
|
15
|
+
} & I18NProps<MobileMenuTogglerI18n> & SSRSupportProps;
|
|
16
|
+
/**
|
|
17
|
+
* A wrapper component that handles conditional hiding/toggling
|
|
18
|
+
* behavior, similar to the one `MainMenu` uses.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MobileMenuToggler: (props: MobileMenuTogglerProps) => JSX.Element;
|
|
21
|
+
export { type MobileMenuTogglerState, useMobileMenuTogglerState, } from './MobileMenuToggler/_useMobileMenuToggling.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
3
|
+
import { MobileMenuStateProvider, useMobileMenuToggling, } from './MobileMenuToggler/_useMobileMenuToggling.js';
|
|
4
|
+
export const defaultMobileMenuTogglerTexts = {
|
|
5
|
+
is: {
|
|
6
|
+
togglerLabel: 'Opna/loka Aðalvalmynd',
|
|
7
|
+
closeMenuLabel: 'Loka',
|
|
8
|
+
closeMenuLabelLong: 'Loka aðalvalmynd',
|
|
9
|
+
},
|
|
10
|
+
en: {
|
|
11
|
+
togglerLabel: 'Toggle Main Menu',
|
|
12
|
+
closeMenuLabel: 'Close',
|
|
13
|
+
closeMenuLabelLong: 'Close main menu',
|
|
14
|
+
},
|
|
15
|
+
pl: {
|
|
16
|
+
togglerLabel: 'Otwórz/zamknij menu główne',
|
|
17
|
+
closeMenuLabel: 'Zamknij',
|
|
18
|
+
closeMenuLabelLong: 'Zamknij menu główne',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A wrapper component that handles conditional hiding/toggling
|
|
23
|
+
* behavior, similar to the one `MainMenu` uses.
|
|
24
|
+
*/
|
|
25
|
+
export const MobileMenuToggler = (props) => {
|
|
26
|
+
const { isMenuActive, isMenuOpen, uiState, closeMenu, toggleMenu } = useMobileMenuToggling({
|
|
27
|
+
doInitialize: props.ssr !== 'ssr-only',
|
|
28
|
+
});
|
|
29
|
+
const txt = getTexts(props, defaultMobileMenuTogglerTexts);
|
|
30
|
+
// const isBrowser = useIsBrowserSide()
|
|
31
|
+
return (React.createElement(React.Fragment, null,
|
|
32
|
+
isMenuActive && (React.createElement("button", { className: "MobileMenuToggler", onClick: toggleMenu, "aria-controls": props.controlsId, "aria-pressed": isMenuOpen }, txt.togglerLabel)),
|
|
33
|
+
React.createElement(MobileMenuStateProvider, { value: uiState }, props.children),
|
|
34
|
+
isMenuActive && (React.createElement("button", { className: "MobileMenuToggler__closebutton", onClick: closeMenu, "aria-label": txt.closeMenuLabelLong, type: "button" }, txt.closeMenuLabel))));
|
|
35
|
+
};
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
export { useMobileMenuTogglerState, } from './MobileMenuToggler/_useMobileMenuToggling.js';
|
package/esm/Multiselect.js
CHANGED
|
@@ -3,11 +3,11 @@ import { modifiedClass } from '@hugsmidjan/qj/classUtils';
|
|
|
3
3
|
import domId from '@hugsmidjan/qj/domid';
|
|
4
4
|
import { notNully } from '@reykjavik/hanna-utils';
|
|
5
5
|
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
6
|
-
import { FocusTrap } from './_abstract/_FocusTrap.js';
|
|
7
6
|
import { filterItems } from './Multiselect/_Multiselect.search.js';
|
|
8
7
|
import { useDomid } from './utils/useDomid.js';
|
|
9
8
|
import { useOnClickOutside } from './utils/useOnClickOutside.js';
|
|
10
9
|
import Checkbox from './Checkbox.js';
|
|
10
|
+
import { FocusTrap } from './FocusTrap.js';
|
|
11
11
|
import FormField, { getFormFieldWrapperProps } from './FormField.js';
|
|
12
12
|
import TagPill from './TagPill.js';
|
|
13
13
|
import { useMixedControlState } from './utils.js';
|
|
@@ -101,9 +101,12 @@ export const Multiselect = (props) => {
|
|
|
101
101
|
const _newValues = isAdding
|
|
102
102
|
? [...values, selValue]
|
|
103
103
|
: values.filter((value) => value !== selValue);
|
|
104
|
-
const selectedValues =
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const selectedValues = [
|
|
105
|
+
// deduplicate selectedValues
|
|
106
|
+
...new Set(options
|
|
107
|
+
.filter((item) => _newValues.includes(item.value))
|
|
108
|
+
.map((item) => item.value)),
|
|
109
|
+
];
|
|
107
110
|
setValues(selectedValues);
|
|
108
111
|
if (onSelected) {
|
|
109
112
|
onSelected({
|
|
@@ -217,6 +220,10 @@ export const Multiselect = (props) => {
|
|
|
217
220
|
removable: true,
|
|
218
221
|
onRemove: () => {
|
|
219
222
|
handleCheckboxSelection(item);
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
224
|
+
inputWrapperRef
|
|
225
|
+
.current.querySelector('.Multiselect__choices')
|
|
226
|
+
.focus();
|
|
220
227
|
},
|
|
221
228
|
}
|
|
222
229
|
: { removable: false }))))))),
|
|
@@ -229,7 +236,7 @@ export const Multiselect = (props) => {
|
|
|
229
236
|
const insertGroupSeparator = !isFiltered &&
|
|
230
237
|
item.group !== (filteredOptions[idx - 1] || {}).group &&
|
|
231
238
|
(idx > 0 || !!item.group);
|
|
232
|
-
const checkbox = (React.createElement(Checkbox, Object.assign({ key: idx, className: modifiedClass('Multiselect__option', activeItemIndex === idx && 'focused'), disabled: isDisabled, readOnly: readOnly, required: props.required, Wrapper: "li", name: name }, item, { checked: isChecked, "aria-invalid": props.invalid, label: item.label || item.value, onChange: () => handleCheckboxSelection(item), onFocus: () => setActiveItemIndex(idx), wrapperProps: {
|
|
239
|
+
const checkbox = (React.createElement(Checkbox, Object.assign({ key: idx, className: modifiedClass('Multiselect__option', activeItemIndex === idx && 'focused'), reqText: false, disabled: isDisabled, readOnly: readOnly, required: props.required, Wrapper: "li", name: name }, item, { checked: isChecked, "aria-invalid": props.invalid, label: item.label || item.value, onChange: () => handleCheckboxSelection(item), onFocus: () => setActiveItemIndex(idx), wrapperProps: {
|
|
233
240
|
onMouseEnter: () => setActiveItemIndex(idx),
|
|
234
241
|
} })));
|
|
235
242
|
return insertGroupSeparator ? (React.createElement(Fragment, { key: idx },
|
package/esm/TagPill.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import React, { Fragment, useEffect, useRef, useState, } from 'react';
|
|
|
2
2
|
import { modifiedClass } from '@hugsmidjan/qj/classUtils';
|
|
3
3
|
import { focusElm } from '@hugsmidjan/qj/focusElm';
|
|
4
4
|
import { getTexts } from '@reykjavik/hanna-utils/i18n';
|
|
5
|
+
import { FocusTrap } from '../FocusTrap.js';
|
|
5
6
|
import { useDomid, useIsBrowserSide, } from '../utils.js';
|
|
6
7
|
import { useCallbackOnEsc } from '../utils/useCallbackOnEsc.js';
|
|
7
|
-
import { FocusTrap } from './_FocusTrap.js';
|
|
8
8
|
import { Portal } from './_Portal.js';
|
|
9
9
|
const MODAL_OPEN_CLASS = 'modal-open';
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
@@ -70,12 +70,17 @@ export const AbstractModal = (props) => {
|
|
|
70
70
|
setTimeout(props.onClosed, closeDelay);
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
+
// ---
|
|
74
|
+
// Update open state when props.open changes. Icky but simple.
|
|
73
75
|
const lastPropsOpen = useRef(props.open);
|
|
74
|
-
// Update state when props.open changes. Icky but simple.
|
|
75
76
|
if (props.open !== lastPropsOpen.current && props.open !== open) {
|
|
76
77
|
lastPropsOpen.current = props.open;
|
|
78
|
+
// these update state during render, which aborts the current render
|
|
79
|
+
// and triggers an immediate rerender.
|
|
77
80
|
props.open ? openModal() : closeModal();
|
|
78
81
|
}
|
|
82
|
+
lastPropsOpen.current = props.open;
|
|
83
|
+
// ---
|
|
79
84
|
const closeOnCurtainClick = isFickle &&
|
|
80
85
|
((e) => {
|
|
81
86
|
if (e.target === e.currentTarget) {
|
|
@@ -2,13 +2,13 @@ import { ReactNode } from 'react';
|
|
|
2
2
|
import { BemModifierProps, BemProps } from '../utils/types.js';
|
|
3
3
|
type ButtonElmProps = {
|
|
4
4
|
href?: never;
|
|
5
|
-
} & BemModifierProps &
|
|
5
|
+
} & BemModifierProps & JSX.IntrinsicElements['button'];
|
|
6
6
|
type AnchorElmProps = {
|
|
7
7
|
href: string;
|
|
8
8
|
type?: never;
|
|
9
9
|
name?: never;
|
|
10
10
|
value?: never;
|
|
11
|
-
} & BemModifierProps &
|
|
11
|
+
} & BemModifierProps & JSX.IntrinsicElements['a'];
|
|
12
12
|
export type ButtonProps = {
|
|
13
13
|
/** Label takes preference over `children` */
|
|
14
14
|
label?: string | JSX.Element;
|
|
@@ -25,7 +25,7 @@ declare const variants: {
|
|
|
25
25
|
};
|
|
26
26
|
type ButtonVariant = keyof typeof variants;
|
|
27
27
|
type NavigationFlag = 'none' | 'go-back' | 'go-forward';
|
|
28
|
-
type ButtonIcon = 'edit';
|
|
28
|
+
export type ButtonIcon = 'edit';
|
|
29
29
|
export type ButtonVariantProps = {
|
|
30
30
|
size?: ButtonSize;
|
|
31
31
|
variant?: ButtonVariant;
|
package/esm/_abstract/_Button.js
CHANGED
|
@@ -28,12 +28,12 @@ export const Button = (props) => {
|
|
|
28
28
|
const { bem, small, // eslint-disable-line deprecation/deprecation
|
|
29
29
|
size = small ? 'small' : 'normal', modifier, children, variant = 'normal', icon = 'none', label = children } = props, buttonProps = __rest(props, ["bem", "small", "size", "modifier", "children", "variant", "icon", "label"]);
|
|
30
30
|
const className = bem &&
|
|
31
|
-
modifiedClass(bem, [modifier, variants[variant], sizes[size], navigationFlags[icon]]);
|
|
31
|
+
modifiedClass(bem, [modifier, variants[variant], sizes[size], navigationFlags[icon]], props.className);
|
|
32
32
|
const iconProp = icons[icon] && { 'data-icon': icons[icon] };
|
|
33
33
|
if (buttonProps.href != null) {
|
|
34
|
-
return (React.createElement(Link, Object.assign({
|
|
34
|
+
return (React.createElement(Link, Object.assign({}, buttonProps, { className: className }, iconProp), label));
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
return (React.createElement("button", Object.assign({
|
|
37
|
+
return (React.createElement("button", Object.assign({ type: "button" }, buttonProps, { className: className }, iconProp), label));
|
|
38
38
|
}
|
|
39
39
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
2
|
import { HTMLProps, WrapperElmProps } from '../utils.js';
|
|
3
3
|
type SectionTag = 'thead' | 'tfoot' | 'tbody';
|
|
4
|
-
type RowPropsFunction = (rowIdx: number, section: SectionTag) => HTMLProps<'tr'> | undefined;
|
|
4
|
+
type RowPropsFunction = (rowIdx: number, section: SectionTag, rowData: Array<TableCellData>) => HTMLProps<'tr'> | undefined;
|
|
5
5
|
export type TableCellMeta = {
|
|
6
6
|
className?: string;
|
|
7
7
|
number?: false;
|
|
@@ -28,7 +28,7 @@ export type TableCellData = {
|
|
|
28
28
|
colSpan?: number;
|
|
29
29
|
key?: string | number;
|
|
30
30
|
} & TableCellMeta;
|
|
31
|
-
type
|
|
31
|
+
type _RowData = {
|
|
32
32
|
cells: Array<TableCellData>;
|
|
33
33
|
key: string | number | undefined;
|
|
34
34
|
};
|
|
@@ -55,7 +55,7 @@ export type TableData = {
|
|
|
55
55
|
tbodies: Array<TableBody>;
|
|
56
56
|
});
|
|
57
57
|
type TableSectionProps = {
|
|
58
|
-
section: Array<
|
|
58
|
+
section: Array<_RowData>;
|
|
59
59
|
cols?: TableCols;
|
|
60
60
|
Tag: SectionTag;
|
|
61
61
|
getRowProps: RowPropsFunction;
|
package/esm/_abstract/_Table.js
CHANGED
|
@@ -20,7 +20,7 @@ const TableCell = (props) => {
|
|
|
20
20
|
};
|
|
21
21
|
const TableSection = ({ section, cols = [], Tag, getRowProps }) => section.length ? (React.createElement(Tag, null, section.map(({ key, cells }, rowIdx) => {
|
|
22
22
|
let colIdx = 0;
|
|
23
|
-
return (React.createElement("tr", Object.assign({}, getRowProps(rowIdx, Tag), { key: key != null ? key : rowIdx }), cells.map((cell, i) => {
|
|
23
|
+
return (React.createElement("tr", Object.assign({}, getRowProps(rowIdx, Tag, cells), { key: key != null ? key : rowIdx }), cells.map((cell, i) => {
|
|
24
24
|
const rowScope = i === 0;
|
|
25
25
|
const meta = cols[colIdx];
|
|
26
26
|
colIdx += cell.colSpan || 1;
|
package/esm/index.d.ts
CHANGED
|
@@ -41,7 +41,9 @@
|
|
|
41
41
|
/// <reference path="./NameCard.d.tsx" />
|
|
42
42
|
/// <reference path="./Multiselect.d.tsx" />
|
|
43
43
|
/// <reference path="./Modal.d.tsx" />
|
|
44
|
+
/// <reference path="./MobileMenuToggler.d.tsx" />
|
|
44
45
|
/// <reference path="./MiniMetrics.d.tsx" />
|
|
46
|
+
/// <reference path="./MainMenu2.d.tsx" />
|
|
45
47
|
/// <reference path="./MainMenu.d.tsx" />
|
|
46
48
|
/// <reference path="./Layout.d.tsx" />
|
|
47
49
|
/// <reference path="./LabeledTextBlock.d.tsx" />
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
/// <reference path="./FooterInfo.d.tsx" />
|
|
63
65
|
/// <reference path="./FooterBadges.d.tsx" />
|
|
64
66
|
/// <reference path="./Foonote.d.tsx" />
|
|
67
|
+
/// <reference path="./FocusTrap.d.tsx" />
|
|
65
68
|
/// <reference path="./FileInput.d.tsx" />
|
|
66
69
|
/// <reference path="./FieldGroup.d.tsx" />
|
|
67
70
|
/// <reference path="./FeatureList.d.tsx" />
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { focusElement } from '@reykjavik/hanna-utils';
|
|
2
|
+
export const handleAnchorLinkClick = (e) => {
|
|
3
|
+
e.preventDefault();
|
|
4
|
+
const targetId = e.currentTarget.hash.slice(1);
|
|
5
|
+
const targetElm = targetId && document.getElementById(targetId);
|
|
6
|
+
if (!targetElm) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
targetElm.tabIndex = -1;
|
|
10
|
+
focusElement(targetElm);
|
|
11
|
+
};
|
package/esm/utils/types.d.ts
CHANGED
|
@@ -13,3 +13,7 @@ export type BemProps<Required extends boolean = false> = BemModifierProps & (Req
|
|
|
13
13
|
/** CSS BEM class-name prefix to be used for this component. Defaults to the same as the original component's displayName */
|
|
14
14
|
bem?: string;
|
|
15
15
|
});
|
|
16
|
+
export type I18NProps<Texts extends Record<string, unknown>> = {
|
|
17
|
+
texts?: Texts;
|
|
18
|
+
lang?: string;
|
|
19
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MediaFormat } from '@reykjavik/hanna-utils';
|
|
1
2
|
/**
|
|
2
3
|
* Pass a callback that gets called whenever the browser window
|
|
3
4
|
* resizes past one of the preconfigured "media-format" breakpoints.
|
|
@@ -25,16 +26,8 @@
|
|
|
25
26
|
* // type Formats = 'phone' | 'phablet' | 'tablet' | 'netbook' | 'wide'
|
|
26
27
|
* media.is // : Format
|
|
27
28
|
* media.was // ?: Format
|
|
28
|
-
* // Category/mode boolean flags
|
|
29
|
-
* // (Hamburger refers to the "mobile menu" mode)
|
|
30
|
-
* media.isTopmenu
|
|
31
|
-
* media.isHamburger
|
|
32
|
-
* media.wasTopmenu
|
|
33
|
-
* media.wasHamburger
|
|
34
|
-
* media.becameTopmenu
|
|
35
|
-
* media.becameHamburger
|
|
36
|
-
* media.leftTopmenu
|
|
37
|
-
* media.leftHamburger
|
|
38
29
|
* ```
|
|
39
30
|
*/
|
|
40
|
-
export declare const useFormatMonitor:
|
|
31
|
+
export declare const useFormatMonitor: HookTypeWithDeprecation;
|
|
32
|
+
type HookTypeWithDeprecation = (callback: (media: MediaFormat) => void) => void;
|
|
33
|
+
export {};
|