@reykjavik/hanna-react 0.10.161 → 0.10.163

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.d.ts CHANGED
@@ -11,8 +11,11 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
11
11
  export declare const alertTypes: Record<string, 1>;
12
12
  export type AlertProps = {
13
13
  type: AlertType;
14
+ /** Defaults to `true` if an `onClose` handler or a `closeUrl` is passaed */
14
15
  closable?: boolean;
16
+ /** The alert content */
15
17
  children?: ReactNode;
18
+ /** UNSAFE raw HTML content for the Alert, e.g. one emitted by a CMS. */
16
19
  childrenHTML?: string;
17
20
  /** server-side anchor href */
18
21
  closeUrl?: string;
@@ -21,8 +24,8 @@ export type AlertProps = {
21
24
  /** Set to true to opt out of the opening transition */
22
25
  instantShow?: boolean;
23
26
  } & SSRSupportProps & EitherObj<{
24
- /** Seconds until the Alert auto-closes.
25
- *
27
+ /**
28
+ * Seconds until the Alert auto-closes.
26
29
  * Mosueover and keyboard focus resets the timer.
27
30
  */
28
31
  autoClose: number;
@@ -31,7 +34,8 @@ export type AlertProps = {
31
34
  /** Callback that fires when the alert has closed/transitoned out */
32
35
  onClosed: () => void;
33
36
  }, {
34
- /** @deprecated This signature with the `event` argument will be removed in hanna-react v0.11
37
+ /**
38
+ * NOTE: The signature with the `event` argument will be removed in hanna-react v0.11
35
39
  *
36
40
  * Return `false` to prevent the alert from closing
37
41
  */
package/Alert.js CHANGED
@@ -48,8 +48,7 @@ exports.alertTypes = {
48
48
  critical: 1,
49
49
  };
50
50
  const Alert = (props) => {
51
- const { type, childrenHTML, children, onClose, // eslint-disable-line deprecation/deprecation
52
- closeUrl, closable = !!(onClose || closeUrl != null), ssr, onClosed, instantShow, wrapperProps, } = props;
51
+ const { type, childrenHTML, children, onClose, closeUrl, closable = !!(onClose || closeUrl != null), ssr, onClosed, instantShow, wrapperProps, } = props;
53
52
  const autoClose = Math.max(props.autoClose || 0, 0);
54
53
  const closing = (0, react_1.useRef)();
55
54
  const isBrowser = (0, utils_js_1.useIsBrowserSide)(ssr);
@@ -62,7 +61,7 @@ const Alert = (props) => {
62
61
  const closeAlert = (0, react_1.useCallback)((event) => {
63
62
  const ret = onClose &&
64
63
  // @ts-expect-error (@deprecated `event` parameter will be removed in v0.11)
65
- onClose(event); // eslint-disable-line deprecation/deprecation
64
+ onClose(event);
66
65
  if (ret !== false) {
67
66
  setOpen(false);
68
67
  if (closing.current) {
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@
4
4
 
5
5
  - ... <!-- Add new lines here. -->
6
6
 
7
+ ## 0.10.163
8
+
9
+ _2026-01-20_
10
+
11
+ - feat: Add `useLaggedState` to `utils` for advanced delayed state updates
12
+
13
+ ## 0.10.162
14
+
15
+ _2026-01-16_
16
+
17
+ - feat: Add component `ContextMenu` (previously `DropdownButton`)
18
+ - feat: Deprecate component `DropdownButton` (in favor of `ContextMenu`)
19
+
7
20
  ## 0.10.161
8
21
 
9
22
  _2026-01-15_
@@ -0,0 +1,86 @@
1
+ import { HTMLAttributeAnchorTarget, ReactElement } from 'react';
2
+ import { IconToken } from '@reykjavik/hanna-css';
3
+ import { ClassNameModifiers, EitherObj } from '@reykjavik/hanna-utils';
4
+ import { IconName_old } from '../../hanna-css/src/lib/icons.js';
5
+ import { ButtonVariantProps } from './_abstract/_Button.js';
6
+ import { SSRSupportProps, WrapperElmProps } from './utils.js';
7
+ type Prefix<record extends Record<string, unknown>, prefix extends string> = {
8
+ [K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
9
+ };
10
+ export type ContextMenuItem = {
11
+ /** Visible label text */
12
+ label: string | ReactElement;
13
+ /** Un-abbreviated label set as `aria-label=""` */
14
+ labelLong?: string;
15
+ /** Language of the link label */
16
+ lang?: string;
17
+ /** Languge of the linked resource */
18
+ hrefLang?: string;
19
+ /**
20
+ * Puts a modifier className for the menu __item <li/> element.
21
+ * */
22
+ modifier?: ClassNameModifiers;
23
+ /** Signifies if the menu item is part of the page's breadcrumb trail */
24
+ current?: boolean;
25
+ /**
26
+ * The URL the link points to.
27
+ *
28
+ * If neither `href` nor `onClick` is passed, then the item is not rendered
29
+ * at all.
30
+ */
31
+ href?: string;
32
+ /** Sets `target=""` on anchor tags with a `href` attribute. */
33
+ target?: HTMLAttributeAnchorTarget;
34
+ /**
35
+ * Adding `onClick` automatically results in a <button/> element being
36
+ * rendered. If `href` is also passed, then a <a href/> element is rendered
37
+ * during initial (server-side) render, which then gets replaced by a
38
+ * <button/> element during the first client-side
39
+ *
40
+ * NOTE: Clicking a menu item will automatically close tghe menu
41
+ * … unless the `onClick` function explicitly returns `false`.
42
+ */
43
+ onClick?: (item: ContextMenuItem) => void | boolean;
44
+ /** Sets `aria-controls=""` on `<button/>`s with `onClick` */
45
+ controlsId?: string;
46
+ Content?: never;
47
+ /** Seldom used flag for buttons that do destruction */
48
+ destructive?: boolean;
49
+ icon?: IconToken | IconName_old;
50
+ };
51
+ type ContextMenuCustomItemFn = (props: {
52
+ closeMenu: () => void;
53
+ }) => ReactElement;
54
+ export type ContextMenuCustomItem = Pick<ContextMenuItem, 'modifier' | 'current'> & {
55
+ Content: ContextMenuCustomItemFn;
56
+ };
57
+ /** Renders a divider line between `ContextMenu*Item`s with an optional legend */
58
+ export type ContextMenuItemDivider = {
59
+ divider: true;
60
+ label?: string;
61
+ };
62
+ export type ContextMenuProps = {
63
+ /** The items to display inside the dropdown menu */
64
+ items: Array<ContextMenuItem | ContextMenuCustomItem | ContextMenuItemDivider>;
65
+ /**
66
+ * NOTE: Clicking a ContextMenu item will automatically close the drropdown
67
+ * … unless the `onItemClick` function explicitly returns `false`.
68
+ *
69
+ * **NOTE:** Customm items will need to call `closeMenu()` themselves.
70
+ */
71
+ onItemClick?: (item: ContextMenuItem) => void | boolean;
72
+ } & EitherObj<{
73
+ /** Label for the toggler */
74
+ label: string | ReactElement;
75
+ /** Longer accessible toggler label text */
76
+ labelLong?: string;
77
+ /** Default: `"secondary"` */
78
+ togglerType?: 'primary' | 'secondary';
79
+ } & Prefix<Omit<ButtonVariantProps, 'small'>, 'toggler'>, {
80
+ /** Custom toggler rendering function component */
81
+ Toggler: (props: {
82
+ isOpen: boolean;
83
+ }) => ReactElement;
84
+ }> & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
85
+ export declare const ContextMenu: (props: ContextMenuProps) => JSX.Element;
86
+ export {};
package/ContextMenu.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContextMenu = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const react_1 = tslib_1.__importStar(require("react"));
6
+ const react_2 = require("@floating-ui/react");
7
+ const hanna_utils_1 = require("@reykjavik/hanna-utils");
8
+ const _Button_js_1 = require("./_abstract/_Button.js");
9
+ const useCallbackOnEsc_js_1 = require("./utils/useCallbackOnEsc.js");
10
+ const useLaggedState_js_1 = require("./utils/useLaggedState.js");
11
+ const useOnClickOutside_js_1 = require("./utils/useOnClickOutside.js");
12
+ const FocusTrap_js_1 = require("./FocusTrap.js");
13
+ const utils_js_1 = require("./utils.js");
14
+ const ContextMenu = (props) => {
15
+ const [isOpen, setIsOpen] = (0, useLaggedState_js_1.useLaggedState)(false, 10);
16
+ const isBrowser = (0, utils_js_1.useIsBrowserSide)(props.ssr);
17
+ const [isHovering, setIsHovering] = (0, react_1.useState)(false);
18
+ const wrapperRef = (0, react_1.useRef)(null);
19
+ const closeMenuStat = () => setIsOpen(false, 0);
20
+ (0, useOnClickOutside_js_1.useOnClickOutside)(wrapperRef, isOpen && closeMenuStat);
21
+ (0, useCallbackOnEsc_js_1.useCallbackOnEsc)(isOpen && closeMenuStat);
22
+ const { x, y, refs } = (0, react_2.useFloating)({
23
+ placement: 'bottom-start',
24
+ middleware: [(0, react_2.flip)(), (0, react_2.shift)()],
25
+ whileElementsMounted: react_2.autoUpdate,
26
+ });
27
+ const { onItemClick, wrapperProps = {} } = props;
28
+ const toggle = (e) => {
29
+ e.preventDefault();
30
+ setIsOpen(!isOpen, 0);
31
+ };
32
+ return (react_1.default.createElement("details", Object.assign({}, wrapperProps, { className: (0, hanna_utils_1.modifiedClass)('ContextMenu', isOpen && 'open', wrapperProps.className), open: isOpen, onBlur: (e) => {
33
+ var _a;
34
+ if (!isHovering) {
35
+ setIsOpen(false, 300);
36
+ }
37
+ (_a = wrapperProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(wrapperProps, e);
38
+ }, ref: (elm) => {
39
+ if (!elm) {
40
+ return;
41
+ }
42
+ wrapperRef.current = elm;
43
+ refs.setReference(elm.querySelector('.ContextMenu__toggler'));
44
+ refs.setFloating(elm.querySelector('.ContextMenu__menu'));
45
+ } }),
46
+ props.Toggler ? (react_1.default.createElement("summary", { className: "ContextMenu__toggler", onClick: toggle },
47
+ react_1.default.createElement(props.Toggler, { isOpen: isOpen }))) : (react_1.default.createElement(_Button_js_1.Button, { as: "summary", className: "ContextMenu__toggler", bem: props.togglerType === 'primary' ? 'ButtonPrimary' : 'ButtonSecondary', icon: props.togglerIcon, size: props.togglerSize, variant: props.togglerVariant, "aria-label": props.labelLong, onClick: toggle }, props.label)),
48
+ react_1.default.createElement("ul", { className: "ContextMenu__menu", onMouseEnter: () => {
49
+ setIsHovering(true);
50
+ }, onMouseLeave: () => {
51
+ setIsHovering(false);
52
+ }, onFocus: () => {
53
+ setIsOpen(true, 0);
54
+ }, style: x != null
55
+ ? {
56
+ '--ContextMenu-pos-y': `${y}px`,
57
+ '--ContextMenu-pos-x': `${x}px`,
58
+ }
59
+ : undefined },
60
+ props.items.map(
61
+ // eslint-disable-next-line complexity
62
+ (item, i) => {
63
+ if ('divider' in item) {
64
+ if ((i === 0 && !item.label) || i === props.items.length - 1) {
65
+ // Gracefully omit pointless dividers
66
+ return null;
67
+ }
68
+ return (react_1.default.createElement("li", { key: i, className: (0, hanna_utils_1.modifiedClass)('ContextMenu__itemDivider', item.label && 'labelled') }, item.label || false));
69
+ }
70
+ if (typeof item === 'function') {
71
+ item = { Content: item };
72
+ }
73
+ const itemProps = {
74
+ className: (0, hanna_utils_1.modifiedClass)('ContextMenu__item', item.modifier),
75
+ 'aria-current': item.current || undefined,
76
+ };
77
+ if ('Content' in item && item.Content) {
78
+ return (react_1.default.createElement("li", Object.assign({ key: i, "data-customitem": "" }, itemProps),
79
+ react_1.default.createElement(item.Content, { closeMenu: closeMenuStat })));
80
+ }
81
+ const { label, onClick, href, destructive } = item;
82
+ const commonProps = {
83
+ className: (0, hanna_utils_1.modifiedClass)('ContextMenu__itembutton', destructive && 'destructive'),
84
+ lang: item.lang,
85
+ 'data-icon': item.icon,
86
+ 'arial-label': item.labelLong,
87
+ };
88
+ const doRenderButton = isBrowser && (onClick || (onItemClick && href == null));
89
+ // TypeScript type-narrowing helper for the onClick callbacks below — because
90
+ // `item` is a variable and could hypothetically change before the click occurs
91
+ const _item = item;
92
+ return (react_1.default.createElement("li", { key: i, className: (0, hanna_utils_1.modifiedClass)('ContextMenu__item', item.modifier), "aria-current": item.current || undefined }, doRenderButton ? (react_1.default.createElement("button", Object.assign({}, commonProps, { type: "button", "aria-controls": item.controlsId, onClick: () => {
93
+ const keepOpen1 = onClick && onClick(_item) === false;
94
+ const keepOpen2 = onItemClick && onItemClick(_item) === false;
95
+ !(keepOpen1 || keepOpen2) && closeMenuStat();
96
+ } }), label)) : href != null ? (react_1.default.createElement("a", Object.assign({}, commonProps, { href: href, hrefLang: item.hrefLang, target: item.target, onClick: () => {
97
+ const keepOpen = onItemClick && onItemClick(_item) === false;
98
+ !keepOpen && closeMenuStat();
99
+ } }), label)) : null));
100
+ }),
101
+ react_1.default.createElement(FocusTrap_js_1.FocusTrap, { Tag: "li", depth: 2 }))));
102
+ };
103
+ exports.ContextMenu = ContextMenu;
@@ -3,12 +3,12 @@ import { IconToken } from '@reykjavik/hanna-css';
3
3
  import { ClassNameModifiers, EitherObj } from '@reykjavik/hanna-utils';
4
4
  import { IconName_old } from '../../hanna-css/src/lib/icons.js';
5
5
  import { ButtonVariantProps } from './_abstract/_Button.js';
6
- import { MainMenu2Item } from './MainMenu2.js';
6
+ import { ContextMenuItem } from './ContextMenu.js';
7
7
  import { SSRSupportProps, WrapperElmProps } from './utils.js';
8
8
  type Prefix<record extends Record<string, unknown>, prefix extends string> = {
9
9
  [K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
10
10
  };
11
- export type DropdownButtonItem = {
11
+ type _DropdownButtonItem = {
12
12
  /** Visible label text */
13
13
  label: string | ReactElement;
14
14
  /** Un-abbreviated label set as `aria-label=""` */
@@ -41,7 +41,7 @@ export type DropdownButtonItem = {
41
41
  * NOTE: Clicking a menu item will automatically close tghe menu
42
42
  * … unless the `onClick` function explicitly returns `false`.
43
43
  */
44
- onClick?: (item: MainMenu2Item) => void | boolean;
44
+ onClick?: (item: _DropdownButtonItem) => void | boolean;
45
45
  /** Sets `aria-controls=""` on `<button/>`s with `onClick` */
46
46
  controlsId?: string;
47
47
  Content?: never;
@@ -49,27 +49,28 @@ export type DropdownButtonItem = {
49
49
  destructive?: boolean;
50
50
  icon?: IconToken | IconName_old;
51
51
  };
52
- type DropdownButtonCustomItemFn = (props: {
52
+ type DropdownbButtonCustomItemFn = (props: {
53
53
  closeMenu: () => void;
54
54
  }) => ReactElement;
55
- export type DropdownButtonCustomItem = Pick<DropdownButtonItem, 'modifier' | 'current'> & {
56
- Content: DropdownButtonCustomItemFn;
55
+ type _DropdownButtonCustomItem = Pick<ContextMenuItem, 'modifier' | 'current'> & {
56
+ Content: DropdownbButtonCustomItemFn;
57
57
  };
58
- /** Renders a divider line between `Dropdown*Item`s with an optional legend */
59
- export type DropdownButtonItemDivider = {
58
+ type _DropdownButtonItemDivider = {
60
59
  divider: true;
61
60
  label?: string;
62
61
  };
63
- export type DropdownButtonProps = {
62
+ type _DropdownButtonProps = {
64
63
  /** The items to display inside the dropdown menu */
65
- items: Array<DropdownButtonItem | DropdownButtonCustomItem | DropdownButtonItemDivider>;
64
+ items: Array<_DropdownButtonItem | _DropdownButtonCustomItem | _DropdownButtonItemDivider>;
66
65
  /**
67
- * NOTE: Clicking a DropdownButton item will automatically close the drropdown
66
+ * NOTE: Clicking a ContextMenu item will automatically close the drropdown
68
67
  * … unless the `onItemClick` function explicitly returns `false`.
68
+ *
69
+ * **NOTE:** Customm items will need to call `closeMenu()` themselves.
69
70
  */
70
- onItemClick?: (item: MainMenu2Item) => void | boolean;
71
+ onItemClick?: (item: ContextMenuItem) => void | boolean;
71
72
  } & EitherObj<{
72
- /** Label for the toggler button */
73
+ /** Label for the toggler */
73
74
  label: string | ReactElement;
74
75
  /** Longer accessible toggler label text */
75
76
  labelLong?: string;
@@ -81,5 +82,14 @@ export type DropdownButtonProps = {
81
82
  isOpen: boolean;
82
83
  }) => ReactElement;
83
84
  }> & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
85
+ /** @deprecated Use `ContextMenu` instead. (Will be removed in v0.11) */
84
86
  export declare const DropdownButton: (props: DropdownButtonProps) => JSX.Element;
87
+ /** @deprecated Use `ContextMenuItem` instead. (Will be removed in v0.11) */
88
+ export type DropdownButtonItem = _DropdownButtonItem;
89
+ /** @deprecated Use `ContextMenuCustomItem` instead. (Will be removed in v0.11) */
90
+ export type DropdownButtonCustomItem = _DropdownButtonCustomItem;
91
+ /** @deprecated Use `ContextMenuItemDivider` instead. (Will be removed in v0.11) */
92
+ export type DropdownButtonItemDivider = _DropdownButtonItemDivider;
93
+ /** @deprecated Use `ContextMenuProps` instead. (Will be removed in v0.11) */
94
+ export type DropdownButtonProps = _DropdownButtonProps;
85
95
  export {};
package/DropdownButton.js CHANGED
@@ -2,6 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DropdownButton = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ // ===========================================================================
6
+ /*
7
+ NOTE:
8
+ THIS COMPONENT IS DEPRECATED AND WILL BE REMOVED IN A FUTURE VERSION.
9
+ DO NOT UPDATE IT OR ADD NEW FEATURES.
10
+
11
+ */
12
+ // ===========================================================================
5
13
  const react_1 = tslib_1.__importStar(require("react"));
6
14
  const react_2 = require("@floating-ui/react");
7
15
  const hanna_utils_1 = require("@reykjavik/hanna-utils");
@@ -11,6 +19,8 @@ const useLaggedState_js_1 = require("./utils/useLaggedState.js");
11
19
  const useOnClickOutside_js_1 = require("./utils/useOnClickOutside.js");
12
20
  const FocusTrap_js_1 = require("./FocusTrap.js");
13
21
  const utils_js_1 = require("./utils.js");
22
+ /** @deprecated Use `ContextMenu` instead. (Will be removed in v0.11) */
23
+ // eslint-disable-next-line deprecation/deprecation
14
24
  const DropdownButton = (props) => {
15
25
  const [isOpen, setIsOpen] = (0, useLaggedState_js_1.useLaggedState)(false, 10);
16
26
  const isBrowser = (0, utils_js_1.useIsBrowserSide)(props.ssr);
package/esm/Alert.d.ts CHANGED
@@ -11,8 +11,11 @@ type AlertType = 'info' | 'success' | 'warning' | 'error' | 'critical';
11
11
  export declare const alertTypes: Record<string, 1>;
12
12
  export type AlertProps = {
13
13
  type: AlertType;
14
+ /** Defaults to `true` if an `onClose` handler or a `closeUrl` is passaed */
14
15
  closable?: boolean;
16
+ /** The alert content */
15
17
  children?: ReactNode;
18
+ /** UNSAFE raw HTML content for the Alert, e.g. one emitted by a CMS. */
16
19
  childrenHTML?: string;
17
20
  /** server-side anchor href */
18
21
  closeUrl?: string;
@@ -21,8 +24,8 @@ export type AlertProps = {
21
24
  /** Set to true to opt out of the opening transition */
22
25
  instantShow?: boolean;
23
26
  } & SSRSupportProps & EitherObj<{
24
- /** Seconds until the Alert auto-closes.
25
- *
27
+ /**
28
+ * Seconds until the Alert auto-closes.
26
29
  * Mosueover and keyboard focus resets the timer.
27
30
  */
28
31
  autoClose: number;
@@ -31,7 +34,8 @@ export type AlertProps = {
31
34
  /** Callback that fires when the alert has closed/transitoned out */
32
35
  onClosed: () => void;
33
36
  }, {
34
- /** @deprecated This signature with the `event` argument will be removed in hanna-react v0.11
37
+ /**
38
+ * NOTE: The signature with the `event` argument will be removed in hanna-react v0.11
35
39
  *
36
40
  * Return `false` to prevent the alert from closing
37
41
  */
package/esm/Alert.js CHANGED
@@ -44,8 +44,7 @@ export const alertTypes = {
44
44
  critical: 1,
45
45
  };
46
46
  export const Alert = (props) => {
47
- const { type, childrenHTML, children, onClose, // eslint-disable-line deprecation/deprecation
48
- closeUrl, closable = !!(onClose || closeUrl != null), ssr, onClosed, instantShow, wrapperProps, } = props;
47
+ const { type, childrenHTML, children, onClose, closeUrl, closable = !!(onClose || closeUrl != null), ssr, onClosed, instantShow, wrapperProps, } = props;
49
48
  const autoClose = Math.max(props.autoClose || 0, 0);
50
49
  const closing = useRef();
51
50
  const isBrowser = useIsBrowserSide(ssr);
@@ -58,7 +57,7 @@ export const Alert = (props) => {
58
57
  const closeAlert = useCallback((event) => {
59
58
  const ret = onClose &&
60
59
  // @ts-expect-error (@deprecated `event` parameter will be removed in v0.11)
61
- onClose(event); // eslint-disable-line deprecation/deprecation
60
+ onClose(event);
62
61
  if (ret !== false) {
63
62
  setOpen(false);
64
63
  if (closing.current) {
@@ -0,0 +1,86 @@
1
+ import { HTMLAttributeAnchorTarget, ReactElement } from 'react';
2
+ import { IconToken } from '@reykjavik/hanna-css';
3
+ import { ClassNameModifiers, EitherObj } from '@reykjavik/hanna-utils';
4
+ import { IconName_old } from '../../hanna-css/src/lib/icons.js';
5
+ import { ButtonVariantProps } from './_abstract/_Button.js';
6
+ import { SSRSupportProps, WrapperElmProps } from './utils.js';
7
+ type Prefix<record extends Record<string, unknown>, prefix extends string> = {
8
+ [K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
9
+ };
10
+ export type ContextMenuItem = {
11
+ /** Visible label text */
12
+ label: string | ReactElement;
13
+ /** Un-abbreviated label set as `aria-label=""` */
14
+ labelLong?: string;
15
+ /** Language of the link label */
16
+ lang?: string;
17
+ /** Languge of the linked resource */
18
+ hrefLang?: string;
19
+ /**
20
+ * Puts a modifier className for the menu __item <li/> element.
21
+ * */
22
+ modifier?: ClassNameModifiers;
23
+ /** Signifies if the menu item is part of the page's breadcrumb trail */
24
+ current?: boolean;
25
+ /**
26
+ * The URL the link points to.
27
+ *
28
+ * If neither `href` nor `onClick` is passed, then the item is not rendered
29
+ * at all.
30
+ */
31
+ href?: string;
32
+ /** Sets `target=""` on anchor tags with a `href` attribute. */
33
+ target?: HTMLAttributeAnchorTarget;
34
+ /**
35
+ * Adding `onClick` automatically results in a <button/> element being
36
+ * rendered. If `href` is also passed, then a <a href/> element is rendered
37
+ * during initial (server-side) render, which then gets replaced by a
38
+ * <button/> element during the first client-side
39
+ *
40
+ * NOTE: Clicking a menu item will automatically close tghe menu
41
+ * … unless the `onClick` function explicitly returns `false`.
42
+ */
43
+ onClick?: (item: ContextMenuItem) => void | boolean;
44
+ /** Sets `aria-controls=""` on `<button/>`s with `onClick` */
45
+ controlsId?: string;
46
+ Content?: never;
47
+ /** Seldom used flag for buttons that do destruction */
48
+ destructive?: boolean;
49
+ icon?: IconToken | IconName_old;
50
+ };
51
+ type ContextMenuCustomItemFn = (props: {
52
+ closeMenu: () => void;
53
+ }) => ReactElement;
54
+ export type ContextMenuCustomItem = Pick<ContextMenuItem, 'modifier' | 'current'> & {
55
+ Content: ContextMenuCustomItemFn;
56
+ };
57
+ /** Renders a divider line between `ContextMenu*Item`s with an optional legend */
58
+ export type ContextMenuItemDivider = {
59
+ divider: true;
60
+ label?: string;
61
+ };
62
+ export type ContextMenuProps = {
63
+ /** The items to display inside the dropdown menu */
64
+ items: Array<ContextMenuItem | ContextMenuCustomItem | ContextMenuItemDivider>;
65
+ /**
66
+ * NOTE: Clicking a ContextMenu item will automatically close the drropdown
67
+ * … unless the `onItemClick` function explicitly returns `false`.
68
+ *
69
+ * **NOTE:** Customm items will need to call `closeMenu()` themselves.
70
+ */
71
+ onItemClick?: (item: ContextMenuItem) => void | boolean;
72
+ } & EitherObj<{
73
+ /** Label for the toggler */
74
+ label: string | ReactElement;
75
+ /** Longer accessible toggler label text */
76
+ labelLong?: string;
77
+ /** Default: `"secondary"` */
78
+ togglerType?: 'primary' | 'secondary';
79
+ } & Prefix<Omit<ButtonVariantProps, 'small'>, 'toggler'>, {
80
+ /** Custom toggler rendering function component */
81
+ Toggler: (props: {
82
+ isOpen: boolean;
83
+ }) => ReactElement;
84
+ }> & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
85
+ export declare const ContextMenu: (props: ContextMenuProps) => JSX.Element;
86
+ export {};
@@ -0,0 +1,98 @@
1
+ import React, { useRef, useState, } from 'react';
2
+ import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react';
3
+ import { modifiedClass } from '@reykjavik/hanna-utils';
4
+ import { Button } from './_abstract/_Button.js';
5
+ import { useCallbackOnEsc } from './utils/useCallbackOnEsc.js';
6
+ import { useLaggedState } from './utils/useLaggedState.js';
7
+ import { useOnClickOutside } from './utils/useOnClickOutside.js';
8
+ import { FocusTrap } from './FocusTrap.js';
9
+ import { useIsBrowserSide } from './utils.js';
10
+ export const ContextMenu = (props) => {
11
+ const [isOpen, setIsOpen] = useLaggedState(false, 10);
12
+ const isBrowser = useIsBrowserSide(props.ssr);
13
+ const [isHovering, setIsHovering] = useState(false);
14
+ const wrapperRef = useRef(null);
15
+ const closeMenuStat = () => setIsOpen(false, 0);
16
+ useOnClickOutside(wrapperRef, isOpen && closeMenuStat);
17
+ useCallbackOnEsc(isOpen && closeMenuStat);
18
+ const { x, y, refs } = useFloating({
19
+ placement: 'bottom-start',
20
+ middleware: [flip(), shift()],
21
+ whileElementsMounted: autoUpdate,
22
+ });
23
+ const { onItemClick, wrapperProps = {} } = props;
24
+ const toggle = (e) => {
25
+ e.preventDefault();
26
+ setIsOpen(!isOpen, 0);
27
+ };
28
+ return (React.createElement("details", Object.assign({}, wrapperProps, { className: modifiedClass('ContextMenu', isOpen && 'open', wrapperProps.className), open: isOpen, onBlur: (e) => {
29
+ var _a;
30
+ if (!isHovering) {
31
+ setIsOpen(false, 300);
32
+ }
33
+ (_a = wrapperProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(wrapperProps, e);
34
+ }, ref: (elm) => {
35
+ if (!elm) {
36
+ return;
37
+ }
38
+ wrapperRef.current = elm;
39
+ refs.setReference(elm.querySelector('.ContextMenu__toggler'));
40
+ refs.setFloating(elm.querySelector('.ContextMenu__menu'));
41
+ } }),
42
+ props.Toggler ? (React.createElement("summary", { className: "ContextMenu__toggler", onClick: toggle },
43
+ React.createElement(props.Toggler, { isOpen: isOpen }))) : (React.createElement(Button, { as: "summary", className: "ContextMenu__toggler", bem: props.togglerType === 'primary' ? 'ButtonPrimary' : 'ButtonSecondary', icon: props.togglerIcon, size: props.togglerSize, variant: props.togglerVariant, "aria-label": props.labelLong, onClick: toggle }, props.label)),
44
+ React.createElement("ul", { className: "ContextMenu__menu", onMouseEnter: () => {
45
+ setIsHovering(true);
46
+ }, onMouseLeave: () => {
47
+ setIsHovering(false);
48
+ }, onFocus: () => {
49
+ setIsOpen(true, 0);
50
+ }, style: x != null
51
+ ? {
52
+ '--ContextMenu-pos-y': `${y}px`,
53
+ '--ContextMenu-pos-x': `${x}px`,
54
+ }
55
+ : undefined },
56
+ props.items.map(
57
+ // eslint-disable-next-line complexity
58
+ (item, i) => {
59
+ if ('divider' in item) {
60
+ if ((i === 0 && !item.label) || i === props.items.length - 1) {
61
+ // Gracefully omit pointless dividers
62
+ return null;
63
+ }
64
+ return (React.createElement("li", { key: i, className: modifiedClass('ContextMenu__itemDivider', item.label && 'labelled') }, item.label || false));
65
+ }
66
+ if (typeof item === 'function') {
67
+ item = { Content: item };
68
+ }
69
+ const itemProps = {
70
+ className: modifiedClass('ContextMenu__item', item.modifier),
71
+ 'aria-current': item.current || undefined,
72
+ };
73
+ if ('Content' in item && item.Content) {
74
+ return (React.createElement("li", Object.assign({ key: i, "data-customitem": "" }, itemProps),
75
+ React.createElement(item.Content, { closeMenu: closeMenuStat })));
76
+ }
77
+ const { label, onClick, href, destructive } = item;
78
+ const commonProps = {
79
+ className: modifiedClass('ContextMenu__itembutton', destructive && 'destructive'),
80
+ lang: item.lang,
81
+ 'data-icon': item.icon,
82
+ 'arial-label': item.labelLong,
83
+ };
84
+ const doRenderButton = isBrowser && (onClick || (onItemClick && href == null));
85
+ // TypeScript type-narrowing helper for the onClick callbacks below — because
86
+ // `item` is a variable and could hypothetically change before the click occurs
87
+ const _item = item;
88
+ return (React.createElement("li", { key: i, className: modifiedClass('ContextMenu__item', item.modifier), "aria-current": item.current || undefined }, doRenderButton ? (React.createElement("button", Object.assign({}, commonProps, { type: "button", "aria-controls": item.controlsId, onClick: () => {
89
+ const keepOpen1 = onClick && onClick(_item) === false;
90
+ const keepOpen2 = onItemClick && onItemClick(_item) === false;
91
+ !(keepOpen1 || keepOpen2) && closeMenuStat();
92
+ } }), label)) : href != null ? (React.createElement("a", Object.assign({}, commonProps, { href: href, hrefLang: item.hrefLang, target: item.target, onClick: () => {
93
+ const keepOpen = onItemClick && onItemClick(_item) === false;
94
+ !keepOpen && closeMenuStat();
95
+ } }), label)) : null));
96
+ }),
97
+ React.createElement(FocusTrap, { Tag: "li", depth: 2 }))));
98
+ };
@@ -3,12 +3,12 @@ import { IconToken } from '@reykjavik/hanna-css';
3
3
  import { ClassNameModifiers, EitherObj } from '@reykjavik/hanna-utils';
4
4
  import { IconName_old } from '../../hanna-css/src/lib/icons.js';
5
5
  import { ButtonVariantProps } from './_abstract/_Button.js';
6
- import { MainMenu2Item } from './MainMenu2.js';
6
+ import { ContextMenuItem } from './ContextMenu.js';
7
7
  import { SSRSupportProps, WrapperElmProps } from './utils.js';
8
8
  type Prefix<record extends Record<string, unknown>, prefix extends string> = {
9
9
  [K in keyof record as `${prefix}${Capitalize<string & K>}`]: record[K];
10
10
  };
11
- export type DropdownButtonItem = {
11
+ type _DropdownButtonItem = {
12
12
  /** Visible label text */
13
13
  label: string | ReactElement;
14
14
  /** Un-abbreviated label set as `aria-label=""` */
@@ -41,7 +41,7 @@ export type DropdownButtonItem = {
41
41
  * NOTE: Clicking a menu item will automatically close tghe menu
42
42
  * … unless the `onClick` function explicitly returns `false`.
43
43
  */
44
- onClick?: (item: MainMenu2Item) => void | boolean;
44
+ onClick?: (item: _DropdownButtonItem) => void | boolean;
45
45
  /** Sets `aria-controls=""` on `<button/>`s with `onClick` */
46
46
  controlsId?: string;
47
47
  Content?: never;
@@ -49,27 +49,28 @@ export type DropdownButtonItem = {
49
49
  destructive?: boolean;
50
50
  icon?: IconToken | IconName_old;
51
51
  };
52
- type DropdownButtonCustomItemFn = (props: {
52
+ type DropdownbButtonCustomItemFn = (props: {
53
53
  closeMenu: () => void;
54
54
  }) => ReactElement;
55
- export type DropdownButtonCustomItem = Pick<DropdownButtonItem, 'modifier' | 'current'> & {
56
- Content: DropdownButtonCustomItemFn;
55
+ type _DropdownButtonCustomItem = Pick<ContextMenuItem, 'modifier' | 'current'> & {
56
+ Content: DropdownbButtonCustomItemFn;
57
57
  };
58
- /** Renders a divider line between `Dropdown*Item`s with an optional legend */
59
- export type DropdownButtonItemDivider = {
58
+ type _DropdownButtonItemDivider = {
60
59
  divider: true;
61
60
  label?: string;
62
61
  };
63
- export type DropdownButtonProps = {
62
+ type _DropdownButtonProps = {
64
63
  /** The items to display inside the dropdown menu */
65
- items: Array<DropdownButtonItem | DropdownButtonCustomItem | DropdownButtonItemDivider>;
64
+ items: Array<_DropdownButtonItem | _DropdownButtonCustomItem | _DropdownButtonItemDivider>;
66
65
  /**
67
- * NOTE: Clicking a DropdownButton item will automatically close the drropdown
66
+ * NOTE: Clicking a ContextMenu item will automatically close the drropdown
68
67
  * … unless the `onItemClick` function explicitly returns `false`.
68
+ *
69
+ * **NOTE:** Customm items will need to call `closeMenu()` themselves.
69
70
  */
70
- onItemClick?: (item: MainMenu2Item) => void | boolean;
71
+ onItemClick?: (item: ContextMenuItem) => void | boolean;
71
72
  } & EitherObj<{
72
- /** Label for the toggler button */
73
+ /** Label for the toggler */
73
74
  label: string | ReactElement;
74
75
  /** Longer accessible toggler label text */
75
76
  labelLong?: string;
@@ -81,5 +82,14 @@ export type DropdownButtonProps = {
81
82
  isOpen: boolean;
82
83
  }) => ReactElement;
83
84
  }> & WrapperElmProps<'details', 'open' | 'name'> & SSRSupportProps;
85
+ /** @deprecated Use `ContextMenu` instead. (Will be removed in v0.11) */
84
86
  export declare const DropdownButton: (props: DropdownButtonProps) => JSX.Element;
87
+ /** @deprecated Use `ContextMenuItem` instead. (Will be removed in v0.11) */
88
+ export type DropdownButtonItem = _DropdownButtonItem;
89
+ /** @deprecated Use `ContextMenuCustomItem` instead. (Will be removed in v0.11) */
90
+ export type DropdownButtonCustomItem = _DropdownButtonCustomItem;
91
+ /** @deprecated Use `ContextMenuItemDivider` instead. (Will be removed in v0.11) */
92
+ export type DropdownButtonItemDivider = _DropdownButtonItemDivider;
93
+ /** @deprecated Use `ContextMenuProps` instead. (Will be removed in v0.11) */
94
+ export type DropdownButtonProps = _DropdownButtonProps;
85
95
  export {};
@@ -1,3 +1,11 @@
1
+ // ===========================================================================
2
+ /*
3
+ NOTE:
4
+ THIS COMPONENT IS DEPRECATED AND WILL BE REMOVED IN A FUTURE VERSION.
5
+ DO NOT UPDATE IT OR ADD NEW FEATURES.
6
+
7
+ */
8
+ // ===========================================================================
1
9
  import React, { useRef, useState, } from 'react';
2
10
  import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react';
3
11
  import { modifiedClass } from '@reykjavik/hanna-utils';
@@ -7,6 +15,8 @@ import { useLaggedState } from './utils/useLaggedState.js';
7
15
  import { useOnClickOutside } from './utils/useOnClickOutside.js';
8
16
  import { FocusTrap } from './FocusTrap.js';
9
17
  import { useIsBrowserSide } from './utils.js';
18
+ /** @deprecated Use `ContextMenu` instead. (Will be removed in v0.11) */
19
+ // eslint-disable-next-line deprecation/deprecation
10
20
  export const DropdownButton = (props) => {
11
21
  const [isOpen, setIsOpen] = useLaggedState(false, 10);
12
22
  const isBrowser = useIsBrowserSide(props.ssr);
package/esm/index.d.ts CHANGED
@@ -77,6 +77,7 @@
77
77
  /// <reference path="./ExtraLinks.d.tsx" />
78
78
  /// <reference path="./DropdownButton.d.tsx" />
79
79
  /// <reference path="./Datepicker.d.tsx" />
80
+ /// <reference path="./ContextMenu.d.tsx" />
80
81
  /// <reference path="./ContentImage.d.tsx" />
81
82
  /// <reference path="./ContentArticle.d.tsx" />
82
83
  /// <reference path="./ContactBubble.d.tsx" />
@@ -1,5 +1,6 @@
1
1
  /**
2
- * A `useState` alternative with in-built support for delayed (debounced) effect.
2
+ * An advanced `useState` alternative with in-built support for delayed
3
+ * (debounced) effect.
3
4
  *
4
5
  * This is especially useful when emulating "focusin"/"focusout" events,
5
6
  * and a less jittery 'onMouseEnter'/'onMouseLeave' behavior.
@@ -1,7 +1,8 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  // ---------------------------------------------------------------------------
3
3
  /**
4
- * A `useState` alternative with in-built support for delayed (debounced) effect.
4
+ * An advanced `useState` alternative with in-built support for delayed
5
+ * (debounced) effect.
5
6
  *
6
7
  * This is especially useful when emulating "focusin"/"focusout" events,
7
8
  * and a less jittery 'onMouseEnter'/'onMouseLeave' behavior.
package/esm/utils.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './utils/useDidChange.js';
5
5
  export * from './utils/useDomid.js';
6
6
  export * from './utils/useFormatMonitor.js';
7
7
  export * from './utils/useGetSVGtext.js';
8
+ export * from './utils/useLaggedState.js';
8
9
  export * from './utils/useMixedControlState.js';
9
10
  export * from './utils/useScrollbarWidthCSSVar.js';
10
11
  /**
package/esm/utils.js CHANGED
@@ -4,6 +4,7 @@ export * from './utils/useDidChange.js';
4
4
  export * from './utils/useDomid.js';
5
5
  export * from './utils/useFormatMonitor.js';
6
6
  export * from './utils/useGetSVGtext.js';
7
+ export * from './utils/useLaggedState.js';
7
8
  export * from './utils/useMixedControlState.js';
8
9
  export * from './utils/useScrollbarWidthCSSVar.js';
9
10
  // ---------------------------------------------------------------------------
package/index.d.ts CHANGED
@@ -77,6 +77,7 @@
77
77
  /// <reference path="./ExtraLinks.d.tsx" />
78
78
  /// <reference path="./DropdownButton.d.tsx" />
79
79
  /// <reference path="./Datepicker.d.tsx" />
80
+ /// <reference path="./ContextMenu.d.tsx" />
80
81
  /// <reference path="./ContentImage.d.tsx" />
81
82
  /// <reference path="./ContentArticle.d.tsx" />
82
83
  /// <reference path="./ContactBubble.d.tsx" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reykjavik/hanna-react",
3
- "version": "0.10.161",
3
+ "version": "0.10.163",
4
4
  "author": "Reykjavík (http://www.reykjavik.is)",
5
5
  "contributors": [
6
6
  "Hugsmiðjan ehf (http://www.hugsmidjan.is)",
@@ -17,7 +17,7 @@
17
17
  "@floating-ui/react": "^0.19.2",
18
18
  "@hugsmidjan/qj": "^4.22.1",
19
19
  "@hugsmidjan/react": "^0.4.32",
20
- "@reykjavik/hanna-css": "^0.4.24",
20
+ "@reykjavik/hanna-css": "^0.4.25",
21
21
  "@reykjavik/hanna-utils": "^0.2.21",
22
22
  "@types/react-autosuggest": "^10.1.0",
23
23
  "@types/react-datepicker": "^4.8.0",
@@ -361,6 +361,10 @@
361
361
  "import": "./esm/Datepicker.js",
362
362
  "require": "./Datepicker.js"
363
363
  },
364
+ "./ContextMenu": {
365
+ "import": "./esm/ContextMenu.js",
366
+ "require": "./ContextMenu.js"
367
+ },
364
368
  "./ContentImage": {
365
369
  "import": "./esm/ContentImage.js",
366
370
  "require": "./ContentImage.js"
@@ -1,5 +1,6 @@
1
1
  /**
2
- * A `useState` alternative with in-built support for delayed (debounced) effect.
2
+ * An advanced `useState` alternative with in-built support for delayed
3
+ * (debounced) effect.
3
4
  *
4
5
  * This is especially useful when emulating "focusin"/"focusout" events,
5
6
  * and a less jittery 'onMouseEnter'/'onMouseLeave' behavior.
@@ -4,7 +4,8 @@ exports.useLaggedState = void 0;
4
4
  const react_1 = require("react");
5
5
  // ---------------------------------------------------------------------------
6
6
  /**
7
- * A `useState` alternative with in-built support for delayed (debounced) effect.
7
+ * An advanced `useState` alternative with in-built support for delayed
8
+ * (debounced) effect.
8
9
  *
9
10
  * This is especially useful when emulating "focusin"/"focusout" events,
10
11
  * and a less jittery 'onMouseEnter'/'onMouseLeave' behavior.
package/utils.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './utils/useDidChange.js';
5
5
  export * from './utils/useDomid.js';
6
6
  export * from './utils/useFormatMonitor.js';
7
7
  export * from './utils/useGetSVGtext.js';
8
+ export * from './utils/useLaggedState.js';
8
9
  export * from './utils/useMixedControlState.js';
9
10
  export * from './utils/useScrollbarWidthCSSVar.js';
10
11
  /**
package/utils.js CHANGED
@@ -8,6 +8,7 @@ tslib_1.__exportStar(require("./utils/useDidChange.js"), exports);
8
8
  tslib_1.__exportStar(require("./utils/useDomid.js"), exports);
9
9
  tslib_1.__exportStar(require("./utils/useFormatMonitor.js"), exports);
10
10
  tslib_1.__exportStar(require("./utils/useGetSVGtext.js"), exports);
11
+ tslib_1.__exportStar(require("./utils/useLaggedState.js"), exports);
11
12
  tslib_1.__exportStar(require("./utils/useMixedControlState.js"), exports);
12
13
  tslib_1.__exportStar(require("./utils/useScrollbarWidthCSSVar.js"), exports);
13
14
  // ---------------------------------------------------------------------------