@datalayer/primer-addons 1.0.7 → 1.0.8

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.
@@ -16,6 +16,8 @@ export type BetterSystemStyleObject = BetterCssProperties | SystemStyleObject |
16
16
  export interface SxProp {
17
17
  sx?: BetterSystemStyleObject;
18
18
  }
19
- declare const sx: (props: SxProp) => import("@styled-system/css").CssFunctionReturnType;
19
+ declare const sx: (props: SxProp & {
20
+ theme?: Record<string, unknown>;
21
+ }) => import("@styled-system/css").CSSObject;
20
22
  export default sx;
21
23
  export { merge };
@@ -5,6 +5,6 @@
5
5
  */
6
6
  import css from '@styled-system/css';
7
7
  import merge from 'deepmerge';
8
- const sx = (props) => css(props.sx);
8
+ const sx = (props) => css(props.sx)(props.theme ?? {});
9
9
  export default sx;
10
10
  export { merge };
@@ -31,8 +31,7 @@ import { ToolbarRenderer } from './ToolbarRenderer';
31
31
  * Position the floating element relative to a DOMRect within an anchor element.
32
32
  */
33
33
  function setFloatingPosition(targetRect, floatingElem, anchorElem, verticalGap = 10, horizontalOffset = 5) {
34
- const scrollerElem = anchorElem.parentElement;
35
- if (targetRect === null || !scrollerElem) {
34
+ if (targetRect === null) {
36
35
  floatingElem.style.opacity = '0';
37
36
  floatingElem.style.transform = 'translateY(-4px)';
38
37
  floatingElem.style.top = '-10000px';
@@ -41,17 +40,21 @@ function setFloatingPosition(targetRect, floatingElem, anchorElem, verticalGap =
41
40
  }
42
41
  const floatingElemRect = floatingElem.getBoundingClientRect();
43
42
  const anchorElementRect = anchorElem.getBoundingClientRect();
44
- const editorScrollerRect = scrollerElem.getBoundingClientRect();
45
- let top = targetRect.top - floatingElemRect.height - verticalGap;
46
- let left = targetRect.left -
47
- horizontalOffset -
48
- (anchorElementRect.left - editorScrollerRect.left);
49
- // Keep within anchor bounds
50
- if (top < anchorElementRect.top) {
51
- top = targetRect.bottom + verticalGap;
43
+ // Convert viewport-relative coordinates to anchor-relative coordinates.
44
+ // The toolbar is position:absolute inside anchorElem (position:relative),
45
+ // so top/left must be relative to the anchor's border box.
46
+ let top = targetRect.top - anchorElementRect.top - floatingElemRect.height - verticalGap;
47
+ let left = targetRect.left - anchorElementRect.left - horizontalOffset;
48
+ // If toolbar would go above the anchor, flip it below the selection
49
+ if (top < 0) {
50
+ top = targetRect.bottom - anchorElementRect.top + verticalGap;
52
51
  }
53
- if (left + floatingElemRect.width > editorScrollerRect.right) {
54
- left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
52
+ // Keep within horizontal bounds
53
+ if (left + floatingElemRect.width > anchorElementRect.width) {
54
+ left = anchorElementRect.width - floatingElemRect.width - horizontalOffset;
55
+ }
56
+ if (left < horizontalOffset) {
57
+ left = horizontalOffset;
55
58
  }
56
59
  floatingElem.style.opacity = '1';
57
60
  floatingElem.style.transform = 'translateY(0)';
@@ -85,6 +88,8 @@ export function FloatingToolbar({ items, extraItems, anchorElement, isVisible, c
85
88
  return;
86
89
  const update = () => updatePosition();
87
90
  window.addEventListener('resize', update);
91
+ // Capture phase catches scroll on any ancestor (page, container, etc.)
92
+ window.addEventListener('scroll', update, true);
88
93
  const scrollerElem = anchorElement?.parentElement;
89
94
  if (scrollerElem) {
90
95
  scrollerElem.addEventListener('scroll', update);
@@ -93,6 +98,7 @@ export function FloatingToolbar({ items, extraItems, anchorElement, isVisible, c
93
98
  update();
94
99
  return () => {
95
100
  window.removeEventListener('resize', update);
101
+ window.removeEventListener('scroll', update, true);
96
102
  if (scrollerElem) {
97
103
  scrollerElem.removeEventListener('scroll', update);
98
104
  }
@@ -44,7 +44,7 @@ export function Toolbar({ items, extraItems, className, disabled, ariaLabel = 'E
44
44
  borderColor: 'border.default',
45
45
  position: 'sticky',
46
46
  top: 0,
47
- zIndex: 2,
47
+ zIndex: 1,
48
48
  }, children: _jsx(ToolbarRenderer, { items: allItems, disabled: disabled, size: "medium" }) }));
49
49
  }
50
50
  export default Toolbar;
@@ -38,7 +38,7 @@ export function ToolbarButton({ item, size = 'medium' }) {
38
38
  iconElement = _jsx("span", { style: { fontSize: 12, fontWeight: 600, lineHeight: 1 }, children: label });
39
39
  }
40
40
  const btnSize = size === 'small' ? 28 : 32;
41
- return (_jsx("button", { type: "button", "aria-label": ariaLabel, title: title, onClick: onClick, disabled: disabled, style: {
41
+ return (_jsx("button", { type: "button", "aria-label": ariaLabel, title: title, onMouseDown: (e) => e.preventDefault(), onClick: onClick, disabled: disabled, style: {
42
42
  display: 'inline-flex',
43
43
  alignItems: 'center',
44
44
  justifyContent: 'center',
@@ -8,34 +8,85 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  *
9
9
  * Uses Primer React ActionMenu for accessible dropdown menus.
10
10
  *
11
+ * The trigger button highlights (accent colour) when any option is active,
12
+ * matching the behavior of regular ToolbarButton items.
13
+ *
14
+ * All option rows render a fixed-width LeadingVisual slot so that labels
15
+ * stay left-aligned regardless of whether the option has an icon.
16
+ *
11
17
  * @module components/toolbar/ToolbarDropdown
12
18
  */
13
- import { isValidElement } from 'react';
19
+ import { isValidElement, useMemo } from 'react';
14
20
  import { ActionMenu, ActionList, Text } from '@primer/react';
15
21
  import { Box } from '../box/Box';
22
+ /**
23
+ * Resolve an icon prop to a ReactNode.
24
+ *
25
+ * Octicon components use React.forwardRef which returns an *object*
26
+ * (typeof === 'object'), NOT a function. We check isValidElement first
27
+ * (already-instantiated JSX), then treat anything else as a component type.
28
+ */
16
29
  function renderIcon(icon) {
17
30
  if (!icon)
18
31
  return null;
19
- if (typeof icon === 'function') {
20
- const IconComp = icon;
21
- return _jsx(IconComp, { size: 16 });
22
- }
23
32
  if (isValidElement(icon)) {
24
33
  return icon;
25
34
  }
26
- return null;
35
+ // Component type: function component, forwardRef, or memo wrapper
36
+ const IconComp = icon;
37
+ return _jsx(IconComp, { size: 16 });
38
+ }
39
+ /** Whether any dropdown option in the list has an icon. */
40
+ function hasAnyIcon(options) {
41
+ return options.some(o => !!o.icon);
27
42
  }
28
43
  export function ToolbarDropdown({ item, size = 'medium' }) {
29
- const { ariaLabel, icon, label, options, disabled } = item;
30
- return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { "aria-label": ariaLabel, disabled: disabled, variant: "invisible", size: size, sx: {
31
- color: 'fg.muted',
32
- fontWeight: 'normal',
33
- fontSize: size === 'small' ? 0 : 1,
34
- px: 2,
35
- '&:hover:not([disabled])': {
36
- bg: 'neutral.muted',
37
- color: 'fg.default',
38
- },
39
- }, children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [renderIcon(icon), label && (_jsx(Text, { sx: { fontSize: size === 'small' ? 0 : 1 }, children: label }))] }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: options.map(option => (_jsxs(ActionList.Item, { onSelect: option.onClick, disabled: option.disabled, active: option.isActive, children: [option.icon && (_jsx(ActionList.LeadingVisual, { children: renderIcon(option.icon) })), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: 3 }, children: [_jsx(Text, { children: option.label }), option.shortcut && (_jsx(Text, { sx: { color: 'fg.subtle', fontSize: 0, fontFamily: 'mono' }, children: option.shortcut }))] })] }, option.key))) }) })] }));
44
+ const { ariaLabel, icon, label, minWidth, options, disabled } = item;
45
+ // Highlight the trigger when any option is active.
46
+ const anyActive = useMemo(() => options.some(o => o.isActive), [options]);
47
+ // If any option has an icon we reserve a leading-visual column for all rows.
48
+ const showLeadingVisual = useMemo(() => hasAnyIcon(options), [options]);
49
+ const btnSize = size === 'small' ? 28 : 32;
50
+ return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsxs("button", { type: "button", "aria-label": ariaLabel, title: ariaLabel, disabled: disabled, style: {
51
+ display: 'inline-flex',
52
+ alignItems: 'center',
53
+ justifyContent: 'center',
54
+ gap: 4,
55
+ minWidth: btnSize,
56
+ height: btnSize,
57
+ padding: label ? '0 8px' : 0,
58
+ margin: 0,
59
+ border: 'none',
60
+ borderRadius: 6,
61
+ cursor: disabled ? 'not-allowed' : 'pointer',
62
+ background: anyActive
63
+ ? 'var(--bgColor-accent-muted, rgba(9,105,218,0.1))'
64
+ : 'transparent',
65
+ color: anyActive
66
+ ? 'var(--fgColor-accent, #0969da)'
67
+ : 'var(--fgColor-muted, #656d76)',
68
+ opacity: disabled ? 0.5 : 1,
69
+ fontSize: size === 'small' ? 12 : 14,
70
+ fontWeight: 'normal',
71
+ fontFamily: 'inherit',
72
+ lineHeight: 1,
73
+ }, onMouseEnter: (e) => {
74
+ if (!disabled) {
75
+ e.currentTarget.style.background =
76
+ 'var(--bgColor-neutral-muted, rgba(175,184,193,0.2))';
77
+ e.currentTarget.style.color = 'var(--fgColor-default, #1f2328)';
78
+ }
79
+ }, onMouseLeave: (e) => {
80
+ e.currentTarget.style.background = anyActive
81
+ ? 'var(--bgColor-accent-muted, rgba(9,105,218,0.1))'
82
+ : 'transparent';
83
+ e.currentTarget.style.color = anyActive
84
+ ? 'var(--fgColor-accent, #0969da)'
85
+ : 'var(--fgColor-muted, #656d76)';
86
+ }, children: [renderIcon(icon), label && (_jsx("span", { style: {
87
+ display: 'inline-block',
88
+ minWidth: minWidth ? minWidth : undefined,
89
+ textAlign: 'left',
90
+ }, children: label }))] }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: options.map(option => (_jsxs(ActionList.Item, { onSelect: option.onClick, disabled: option.disabled, active: option.isActive, children: [showLeadingVisual && (_jsx(ActionList.LeadingVisual, { children: option.icon ? renderIcon(option.icon) : _jsx(Box, { sx: { width: 16 } }) })), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: 3 }, children: [_jsx(Text, { children: option.label }), option.shortcut && (_jsx(Text, { sx: { color: 'fg.subtle', fontSize: 0, fontFamily: 'mono' }, children: option.shortcut }))] })] }, option.key))) }) })] }));
40
91
  }
41
92
  export default ToolbarDropdown;
@@ -73,6 +73,12 @@ export interface ToolbarDropdownItem extends ToolbarItemBase {
73
73
  icon?: React.ComponentType<IconProps> | ReactNode;
74
74
  /** Text label for the trigger */
75
75
  label?: string;
76
+ /**
77
+ * Fixed minimum width (in px) for the trigger's label area.
78
+ * Set this to the width of the longest possible label so the
79
+ * toolbar doesn't reflow when the selection changes.
80
+ */
81
+ minWidth?: number;
76
82
  /** Dropdown options */
77
83
  options: ToolbarDropdownOption[];
78
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/primer-addons",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "license": "BSD-3-Clause",
5
5
  "scripts": {
6
6
  "build": "tsc && vite build",