@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.
- package/lib/components/box/sx.d.ts +3 -1
- package/lib/components/box/sx.js +1 -1
- package/lib/components/toolbar/FloatingToolbar.js +18 -12
- package/lib/components/toolbar/Toolbar.js +1 -1
- package/lib/components/toolbar/ToolbarButton.js +1 -1
- package/lib/components/toolbar/ToolbarDropdown.js +68 -17
- package/lib/components/toolbar/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -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
|
|
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 };
|
package/lib/components/box/sx.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
if (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
|
-
|
|
54
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|