@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.
Files changed (72) hide show
  1. package/Alert.js +1 -1
  2. package/BasicTable.js +4 -4
  3. package/CHANGELOG.md +34 -1
  4. package/ContactBubble.js +8 -3
  5. package/Datepicker.d.ts +1 -0
  6. package/Datepicker.js +51 -13
  7. package/{_abstract/_FocusTrap.d.ts → FocusTrap.d.ts} +5 -1
  8. package/{_abstract/_FocusTrap.js → FocusTrap.js} +5 -1
  9. package/Layout.d.ts +7 -3
  10. package/Layout.js +7 -26
  11. package/MainMenu.d.ts +52 -20
  12. package/MainMenu.js +27 -6
  13. package/MainMenu2.d.ts +114 -0
  14. package/MainMenu2.js +235 -0
  15. package/MobileMenuToggler/_useMobileMenuToggling.d.ts +21 -0
  16. package/{utils/useMenuToggling.js → MobileMenuToggler/_useMobileMenuToggling.js} +34 -14
  17. package/MobileMenuToggler.d.ts +21 -0
  18. package/MobileMenuToggler.js +43 -0
  19. package/Multiselect.js +13 -6
  20. package/TagPill.d.ts +2 -0
  21. package/_abstract/_AbstractModal.js +9 -4
  22. package/_abstract/_Button.d.ts +3 -3
  23. package/_abstract/_Button.js +3 -3
  24. package/_abstract/_Table.d.ts +3 -3
  25. package/_abstract/_Table.js +1 -1
  26. package/esm/Alert.js +1 -1
  27. package/esm/BasicTable.js +4 -4
  28. package/esm/ContactBubble.js +8 -3
  29. package/esm/Datepicker.d.ts +1 -0
  30. package/esm/Datepicker.js +51 -13
  31. package/esm/{_abstract/_FocusTrap.d.ts → FocusTrap.d.ts} +5 -1
  32. package/esm/{_abstract/_FocusTrap.js → FocusTrap.js} +5 -1
  33. package/esm/Layout.d.ts +7 -3
  34. package/esm/Layout.js +8 -27
  35. package/esm/MainMenu.d.ts +52 -20
  36. package/esm/MainMenu.js +27 -7
  37. package/esm/MainMenu2.d.ts +114 -0
  38. package/esm/MainMenu2.js +230 -0
  39. package/esm/MobileMenuToggler/_useMobileMenuToggling.d.ts +21 -0
  40. package/esm/{utils/useMenuToggling.js → MobileMenuToggler/_useMobileMenuToggling.js} +32 -13
  41. package/esm/MobileMenuToggler.d.ts +21 -0
  42. package/esm/MobileMenuToggler.js +37 -0
  43. package/esm/Multiselect.js +12 -5
  44. package/esm/TagPill.d.ts +2 -0
  45. package/esm/_abstract/_AbstractModal.js +7 -2
  46. package/esm/_abstract/_Button.d.ts +3 -3
  47. package/esm/_abstract/_Button.js +3 -3
  48. package/esm/_abstract/_Table.d.ts +3 -3
  49. package/esm/_abstract/_Table.js +1 -1
  50. package/esm/index.d.ts +3 -0
  51. package/esm/utils/a11yHelpers.d.ts +2 -0
  52. package/esm/utils/a11yHelpers.js +11 -0
  53. package/esm/utils/types.d.ts +4 -0
  54. package/esm/utils/useFormatMonitor.d.ts +4 -11
  55. package/esm/utils/useFormatMonitor.js +0 -10
  56. package/esm/utils.d.ts +7 -2
  57. package/esm/utils.js +8 -2
  58. package/index.d.ts +3 -0
  59. package/package.json +13 -1
  60. package/utils/a11yHelpers.d.ts +2 -0
  61. package/utils/a11yHelpers.js +15 -0
  62. package/utils/types.d.ts +4 -0
  63. package/utils/useFormatMonitor.d.ts +4 -11
  64. package/utils/useFormatMonitor.js +0 -10
  65. package/utils.d.ts +7 -2
  66. package/utils.js +9 -5
  67. package/esm/utils/HannaUIState.d.ts +0 -7
  68. package/esm/utils/HannaUIState.js +0 -7
  69. package/esm/utils/useMenuToggling.d.ts +0 -10
  70. package/utils/HannaUIState.d.ts +0 -7
  71. package/utils/HannaUIState.js +0 -11
  72. package/utils/useMenuToggling.d.ts +0 -10
@@ -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 './useFormatMonitor.js';
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 useMenuToggling = (doInitialize = true) => {
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: () => doInitialize && _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
- focusElement('#pagenav');
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('.Layout__header__skiplink');
57
+ focusElement(togglerElm);
48
58
  }
49
59
  };
50
60
  useFormatMonitor(doInitialize
51
61
  ? (media) => {
52
- if (media.becameHamburger) {
53
- setMenuState((state) => (Object.assign(Object.assign({}, state), { isMenuActive: true })));
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 (media.leftHamburger) {
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';
@@ -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 = options
105
- .filter((item) => _newValues.includes(item.value))
106
- .map((item) => item.value);
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
@@ -10,6 +10,8 @@ declare const colors: {
10
10
  };
11
11
  export type TagPillColor = keyof typeof colors;
12
12
  export type TagPillProps = ButtonProps & {
13
+ /** Not allowed */
14
+ className?: never;
13
15
  children?: ReactNode;
14
16
  large?: boolean;
15
17
  color?: TagPillColor;
@@ -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 & Omit<JSX.IntrinsicElements['button'], 'className' | 'style'>;
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 & Omit<JSX.IntrinsicElements['a'], 'className' | 'style'>;
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;
@@ -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({ className: className }, buttonProps, { style: undefined }, iconProp), label));
34
+ return (React.createElement(Link, Object.assign({}, buttonProps, { className: className }, iconProp), label));
35
35
  }
36
36
  else {
37
- return (React.createElement("button", Object.assign({ className: className, type: "button" }, buttonProps, { style: undefined }, iconProp), label));
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 RowData = {
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<RowData>;
58
+ section: Array<_RowData>;
59
59
  cols?: TableCols;
60
60
  Tag: SectionTag;
61
61
  getRowProps: RowPropsFunction;
@@ -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,2 @@
1
+ import { MouseEventHandler } from 'react';
2
+ export declare const handleAnchorLinkClick: MouseEventHandler<HTMLAnchorElement>;
@@ -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
+ };
@@ -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: (callback: ((media: import("formatchange").BaseMedia & Record<"isHamburger" | "isTopmenu" | "wasHamburger" | "wasTopmenu" | "becameHamburger" | "becameTopmenu" | "leftHamburger" | "leftTopmenu", boolean>) => void) | undefined) => void;
31
+ export declare const useFormatMonitor: HookTypeWithDeprecation;
32
+ type HookTypeWithDeprecation = (callback: (media: MediaFormat) => void) => void;
33
+ export {};