@drivenets/design-system 0.19.0 → 0.20.0
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/CHANGELOG.md +28 -0
- package/dist/index.cjs +12409 -10548
- package/dist/index.d.cts +678 -312
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +679 -313
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12404 -10550
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +29 -29
- package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
- package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
- package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
- package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
- package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
- package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
- package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
- package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
- package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
- package/src/components/ds-code-input/ds-code-input.tsx +401 -0
- package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
- package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
- package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
- package/src/components/ds-code-input/index.ts +2 -0
- package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
- package/src/components/ds-code-input/use-editor-host.ts +87 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
- package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
- package/src/components/ds-filters-bar/components/index.ts +11 -0
- package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
- package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
- package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
- package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
- package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
- package/src/components/ds-filters-bar/index.ts +1 -0
- package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
- package/src/components/ds-form-control/ds-form-control.tsx +43 -9
- package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
- package/src/components/ds-icon/custom-icons/index.ts +2 -0
- package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
- package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
- package/src/components/ds-pin-toggle/index.ts +2 -0
- package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
- package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
- package/src/components/ds-popover/ds-popover.context.ts +18 -0
- package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
- package/src/components/ds-popover/ds-popover.tsx +163 -41
- package/src/components/ds-popover/ds-popover.types.ts +59 -1
- package/src/components/ds-popover/ds-popover.utils.ts +60 -0
- package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
- package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
- package/src/components/ds-saved-filters/components/index.ts +2 -0
- package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
- package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
- package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
- package/src/components/ds-saved-filters/hooks/index.ts +1 -0
- package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
- package/src/components/ds-saved-filters/index.ts +2 -0
- package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
- package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
- package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
- package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
- package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
- package/src/components/ds-tag/ds-tag.figma.ts +23 -3
- package/src/components/ds-tag/ds-tag.module.scss +71 -7
- package/src/components/ds-tag/ds-tag.tsx +115 -14
- package/src/components/ds-tag/ds-tag.types.ts +70 -3
- package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
- package/src/components/ds-tag/index.ts +2 -2
- package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
- package/src/components/ds-toggle/ds-toggle.tsx +19 -15
- package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
- package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
- package/src/components/ds-toggle-filter-data/index.ts +1 -0
- package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
- package/src/components/ds-tooltip/ds-tooltip.tsx +1 -1
- package/src/components/ds-tree/ds-tree.tsx +10 -10
- package/src/components/ds-tree/ds-tree.types.ts +40 -7
- package/src/index.ts +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PopoverTrigger } from './popover-trigger';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Popover } from '@ark-ui/react/popover';
|
|
2
|
+
import { useHoverTriggerProps } from '../../ds-popover.hover-intent';
|
|
3
|
+
import type { DsPopoverTriggerProps } from '../../ds-popover.types';
|
|
4
|
+
|
|
5
|
+
export const PopoverTrigger = ({ children, className }: DsPopoverTriggerProps) => {
|
|
6
|
+
const hoverProps = useHoverTriggerProps();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Popover.Trigger asChild className={className} {...hoverProps}>
|
|
10
|
+
{children}
|
|
11
|
+
</Popover.Trigger>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
PopoverTrigger.displayName = 'DsPopover.Trigger';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { HoverIntent } from './ds-popover.hover-intent';
|
|
3
|
+
|
|
4
|
+
export interface DsPopoverContextValue {
|
|
5
|
+
matchAnchorWidth: boolean;
|
|
6
|
+
registerAnchor: (el: HTMLElement | null) => void;
|
|
7
|
+
registerContentId: (id: string | undefined) => void;
|
|
8
|
+
hoverIntent: HoverIntent | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const DsPopoverContext = createContext<DsPopoverContextValue>({
|
|
12
|
+
matchAnchorWidth: false,
|
|
13
|
+
registerAnchor: () => undefined,
|
|
14
|
+
registerContentId: () => undefined,
|
|
15
|
+
hoverIntent: null,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const useDsPopoverContext = () => useContext(DsPopoverContext);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { FocusEvent, PointerEvent } from 'react';
|
|
2
|
+
import { usePopoverContext } from '@ark-ui/react/popover';
|
|
3
|
+
import { useDsPopoverContext } from './ds-popover.context';
|
|
4
|
+
import { isHoverPointer } from './ds-popover.utils';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_OPEN_DELAY_MS = 200;
|
|
7
|
+
export const DEFAULT_CLOSE_DELAY_MS = 150;
|
|
8
|
+
|
|
9
|
+
export interface HoverIntent {
|
|
10
|
+
openDelay: number;
|
|
11
|
+
closeDelay: number;
|
|
12
|
+
setFocusInPanel: (inPanel: boolean) => void;
|
|
13
|
+
/** True once if the trigger is about to be re-focused by Ark's close-time restore. */
|
|
14
|
+
consumeFocusRestore: () => boolean;
|
|
15
|
+
/** Owns one shared timer */
|
|
16
|
+
schedule: (action: () => void, delay: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const useHoverIntent = () => useDsPopoverContext().hoverIntent;
|
|
20
|
+
|
|
21
|
+
export const useHoverIntentProps = () => {
|
|
22
|
+
const intent = useHoverIntent();
|
|
23
|
+
const { setOpen } = usePopoverContext();
|
|
24
|
+
|
|
25
|
+
if (!intent) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const onPointerIntent = (event: PointerEvent, open: boolean, delay: number) => {
|
|
30
|
+
if (!isHoverPointer(event.pointerType)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
intent.schedule(() => setOpen(open), delay);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
onPointerEnter: (event: PointerEvent) => onPointerIntent(event, true, intent.openDelay),
|
|
39
|
+
onPointerLeave: (event: PointerEvent) => onPointerIntent(event, false, intent.closeDelay),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const useHoverTriggerProps = () => {
|
|
44
|
+
const pointerProps = useHoverIntentProps();
|
|
45
|
+
const intent = useHoverIntent();
|
|
46
|
+
const { setOpen } = usePopoverContext();
|
|
47
|
+
|
|
48
|
+
if (!intent) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...pointerProps,
|
|
54
|
+
onFocus: (event: FocusEvent<HTMLElement>) => {
|
|
55
|
+
// Closing with focus inside the panel makes Ark re-focus the trigger
|
|
56
|
+
if (intent.consumeFocusRestore()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (event.target.matches(':focus-visible')) {
|
|
61
|
+
setOpen(true);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, type FocusEvent } from 'react';
|
|
2
|
+
import { Popover, type PopoverRootProps } from '@ark-ui/react/popover';
|
|
2
3
|
import { Portal } from '@ark-ui/react/portal';
|
|
3
4
|
import classNames from 'classnames';
|
|
4
5
|
import { DsStack } from '../ds-stack';
|
|
5
6
|
import { DsTypography } from '../ds-typography';
|
|
7
|
+
import { PopoverTrigger } from './components/popover-trigger';
|
|
8
|
+
import { DsPopoverContext, useDsPopoverContext } from './ds-popover.context';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CLOSE_DELAY_MS,
|
|
11
|
+
DEFAULT_OPEN_DELAY_MS,
|
|
12
|
+
useHoverIntentProps,
|
|
13
|
+
} from './ds-popover.hover-intent';
|
|
14
|
+
import { invokeCloseAutoFocus, toFocusEl, toPlacement } from './ds-popover.utils';
|
|
6
15
|
import styles from './ds-popover.module.scss';
|
|
7
16
|
import type {
|
|
8
|
-
|
|
17
|
+
DsPopoverAnchorProps,
|
|
9
18
|
DsPopoverContentItemProps,
|
|
10
19
|
DsPopoverContentProps,
|
|
11
20
|
DsPopoverFooterProps,
|
|
@@ -13,14 +22,10 @@ import type {
|
|
|
13
22
|
DsPopoverPanelProps,
|
|
14
23
|
DsPopoverProps,
|
|
15
24
|
DsPopoverRootProps,
|
|
16
|
-
DsPopoverSide,
|
|
17
|
-
DsPopoverTriggerProps,
|
|
18
25
|
} from './ds-popover.types';
|
|
19
26
|
|
|
20
27
|
const DEFAULT_PANEL_WIDTH = 400;
|
|
21
|
-
|
|
22
|
-
export const toPlacement = (side: DsPopoverSide, align: DsPopoverAlign) =>
|
|
23
|
-
align === 'center' ? side : (`${side}-${align}` as const);
|
|
28
|
+
const MATCHED_PANEL_WIDTH = 'var(--reference-width)';
|
|
24
29
|
|
|
25
30
|
const DsPopoverRoot = ({
|
|
26
31
|
open,
|
|
@@ -29,48 +34,164 @@ const DsPopoverRoot = ({
|
|
|
29
34
|
align = 'center',
|
|
30
35
|
gutter = 8,
|
|
31
36
|
modal = false,
|
|
37
|
+
matchAnchorWidth = false,
|
|
38
|
+
restoreFocus,
|
|
39
|
+
openOn = 'click',
|
|
40
|
+
openDelay = DEFAULT_OPEN_DELAY_MS,
|
|
41
|
+
closeDelay = DEFAULT_CLOSE_DELAY_MS,
|
|
32
42
|
getAnchorElement,
|
|
33
43
|
children,
|
|
44
|
+
onOpenAutoFocus,
|
|
45
|
+
onCloseAutoFocus,
|
|
46
|
+
onInteractOutside,
|
|
34
47
|
onOpenChange,
|
|
35
|
-
}: DsPopoverRootProps) =>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
onOpenChange={(details) => onOpenChange?.(details.open)}
|
|
42
|
-
>
|
|
43
|
-
{children}
|
|
44
|
-
</Popover.Root>
|
|
45
|
-
);
|
|
48
|
+
}: DsPopoverRootProps) => {
|
|
49
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
50
|
+
const anchorRef = useRef<HTMLElement | null>(null);
|
|
51
|
+
const restoringFocus = useRef(false);
|
|
52
|
+
const [contentId, setContentId] = useState<string>();
|
|
53
|
+
const [focusInPanel, setFocusInPanel] = useState(false);
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
55
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
56
|
+
|
|
57
|
+
const schedule = (action: () => void, delay: number) => {
|
|
58
|
+
clearTimeout(timer.current);
|
|
59
|
+
timer.current = setTimeout(action, delay);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isHover = openOn === 'hover';
|
|
63
|
+
|
|
64
|
+
const consumeFocusRestore = () => {
|
|
65
|
+
const restoring = restoringFocus.current;
|
|
66
|
+
restoringFocus.current = false;
|
|
67
|
+
|
|
68
|
+
return restoring;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const registerAnchor = (el: HTMLElement | null) => {
|
|
72
|
+
anchorRef.current = el;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ownsCloseFocus = restoreFocus === false && Boolean(onCloseAutoFocus);
|
|
76
|
+
const initialFocusEl = toFocusEl(onOpenAutoFocus, 'openAutoFocus') as PopoverRootProps['initialFocusEl'];
|
|
77
|
+
const finalFocusEl = ownsCloseFocus
|
|
78
|
+
? undefined
|
|
79
|
+
: (toFocusEl(onCloseAutoFocus, 'closeAutoFocus') as PopoverRootProps['finalFocusEl']);
|
|
80
|
+
const arkRestoreFocus = ownsCloseFocus ? false : (restoreFocus ?? (!isHover || focusInPanel));
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<DsPopoverContext.Provider
|
|
84
|
+
value={{
|
|
85
|
+
matchAnchorWidth,
|
|
86
|
+
registerAnchor,
|
|
87
|
+
registerContentId: setContentId,
|
|
88
|
+
hoverIntent: isHover
|
|
89
|
+
? { openDelay, closeDelay, schedule, setFocusInPanel, consumeFocusRestore }
|
|
90
|
+
: null,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<Popover.Root
|
|
94
|
+
open={open}
|
|
95
|
+
defaultOpen={defaultOpen}
|
|
96
|
+
modal={modal}
|
|
97
|
+
ids={contentId ? { content: contentId } : undefined}
|
|
98
|
+
// Trigger focus is lost when open on hover
|
|
99
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
100
|
+
autoFocus={!isHover}
|
|
101
|
+
restoreFocus={arkRestoreFocus}
|
|
102
|
+
positioning={{
|
|
103
|
+
placement: toPlacement(side, align),
|
|
104
|
+
gutter,
|
|
105
|
+
sameWidth: matchAnchorWidth,
|
|
106
|
+
...(getAnchorElement ? { getAnchorElement } : {}),
|
|
107
|
+
}}
|
|
108
|
+
initialFocusEl={initialFocusEl}
|
|
109
|
+
finalFocusEl={finalFocusEl}
|
|
110
|
+
persistentElements={[() => anchorRef.current]}
|
|
111
|
+
onInteractOutside={
|
|
112
|
+
onInteractOutside
|
|
113
|
+
? (event) => {
|
|
114
|
+
const dsEvent = new Event('interactOutside', { cancelable: true });
|
|
115
|
+
onInteractOutside(dsEvent);
|
|
116
|
+
|
|
117
|
+
if (dsEvent.defaultPrevented) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
: undefined
|
|
122
|
+
}
|
|
123
|
+
onOpenChange={(details) => {
|
|
124
|
+
if (!details.open && focusInPanel) {
|
|
125
|
+
restoringFocus.current = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!details.open && ownsCloseFocus && onCloseAutoFocus) {
|
|
129
|
+
invokeCloseAutoFocus(onCloseAutoFocus);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onOpenChange?.(details.open);
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</Popover.Root>
|
|
137
|
+
</DsPopoverContext.Provider>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const DsPopoverAnchor = ({ children, className }: DsPopoverAnchorProps) => {
|
|
142
|
+
const { registerAnchor } = useDsPopoverContext();
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Popover.Anchor asChild className={className} ref={registerAnchor}>
|
|
146
|
+
{children}
|
|
147
|
+
</Popover.Anchor>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
52
150
|
|
|
53
151
|
const DsPopoverPanel = ({
|
|
54
|
-
width
|
|
152
|
+
width,
|
|
55
153
|
className,
|
|
56
154
|
style,
|
|
57
155
|
children,
|
|
58
156
|
ref,
|
|
157
|
+
id,
|
|
59
158
|
'aria-label': ariaLabel,
|
|
60
|
-
}: DsPopoverPanelProps) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
159
|
+
}: DsPopoverPanelProps) => {
|
|
160
|
+
const { matchAnchorWidth, registerContentId, hoverIntent } = useDsPopoverContext();
|
|
161
|
+
const hoverProps = useHoverIntentProps();
|
|
162
|
+
const resolvedWidth = width ?? (matchAnchorWidth ? MATCHED_PANEL_WIDTH : DEFAULT_PANEL_WIDTH);
|
|
163
|
+
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
registerContentId(id);
|
|
166
|
+
|
|
167
|
+
return () => registerContentId(undefined);
|
|
168
|
+
}, [id, registerContentId]);
|
|
169
|
+
|
|
170
|
+
const onFocus = () => hoverIntent?.setFocusInPanel(true);
|
|
171
|
+
const onBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
172
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
173
|
+
hoverIntent?.setFocusInPanel(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Portal>
|
|
179
|
+
<Popover.Positioner>
|
|
180
|
+
<Popover.Content
|
|
181
|
+
{...hoverProps}
|
|
182
|
+
onFocus={onFocus}
|
|
183
|
+
onBlur={onBlur}
|
|
184
|
+
ref={ref}
|
|
185
|
+
aria-label={ariaLabel}
|
|
186
|
+
className={classNames(styles.panel, className)}
|
|
187
|
+
style={{ ...style, width: resolvedWidth }}
|
|
188
|
+
>
|
|
189
|
+
{children}
|
|
190
|
+
</Popover.Content>
|
|
191
|
+
</Popover.Positioner>
|
|
192
|
+
</Portal>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
74
195
|
|
|
75
196
|
const DsPopoverHeader = ({ icon, className, style, children }: DsPopoverHeaderProps) => (
|
|
76
197
|
<div className={classNames(styles.header, className)} style={style}>
|
|
@@ -142,14 +263,14 @@ const DsPopoverLegacy = ({
|
|
|
142
263
|
align = 'center',
|
|
143
264
|
}: DsPopoverProps) => (
|
|
144
265
|
<DsPopoverRoot side={side} align={align}>
|
|
145
|
-
<
|
|
266
|
+
<PopoverTrigger>{trigger}</PopoverTrigger>
|
|
146
267
|
<DsPopoverPanel className={className}>{children}</DsPopoverPanel>
|
|
147
268
|
</DsPopoverRoot>
|
|
148
269
|
);
|
|
149
270
|
|
|
150
271
|
DsPopoverLegacy.displayName = 'DsPopover';
|
|
151
272
|
DsPopoverRoot.displayName = 'DsPopover.Root';
|
|
152
|
-
|
|
273
|
+
DsPopoverAnchor.displayName = 'DsPopover.Anchor';
|
|
153
274
|
DsPopoverPanel.displayName = 'DsPopover.Panel';
|
|
154
275
|
DsPopoverHeader.displayName = 'DsPopover.Header';
|
|
155
276
|
DsPopoverContent.displayName = 'DsPopover.Content';
|
|
@@ -158,7 +279,8 @@ DsPopoverFooter.displayName = 'DsPopover.Footer';
|
|
|
158
279
|
|
|
159
280
|
export const DsPopover = Object.assign(DsPopoverLegacy, {
|
|
160
281
|
Root: DsPopoverRoot,
|
|
161
|
-
Trigger:
|
|
282
|
+
Trigger: PopoverTrigger,
|
|
283
|
+
Anchor: DsPopoverAnchor,
|
|
162
284
|
Panel: DsPopoverPanel,
|
|
163
285
|
Header: DsPopoverHeader,
|
|
164
286
|
Content: DsPopoverContent,
|
|
@@ -6,6 +6,9 @@ export type DsPopoverSide = (typeof popoverSides)[number];
|
|
|
6
6
|
export const popoverAligns = ['start', 'center', 'end'] as const;
|
|
7
7
|
export type DsPopoverAlign = (typeof popoverAligns)[number];
|
|
8
8
|
|
|
9
|
+
export const popoverOpenTriggers = ['click', 'hover'] as const;
|
|
10
|
+
export type DsPopoverOpenTrigger = (typeof popoverOpenTriggers)[number];
|
|
11
|
+
|
|
9
12
|
export interface DsPopoverRootProps {
|
|
10
13
|
/** Controlled open state. Pair with `onOpenChange`. */
|
|
11
14
|
open?: boolean;
|
|
@@ -31,14 +34,62 @@ export interface DsPopoverRootProps {
|
|
|
31
34
|
* @default false
|
|
32
35
|
*/
|
|
33
36
|
modal?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Size the panel to the positioning reference (Anchor, or the trigger).
|
|
39
|
+
* An explicit `DsPopover.Panel` `width` wins over this.
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
matchAnchorWidth?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether to restore focus to the previously focused element on close.
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
restoreFocus?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Trigger open method - click / hover
|
|
50
|
+
* @default 'click'
|
|
51
|
+
*/
|
|
52
|
+
openOn?: DsPopoverOpenTrigger;
|
|
53
|
+
/**
|
|
54
|
+
* Milliseconds the pointer must rest on the trigger before the panel opens.
|
|
55
|
+
* Ignored unless `openOn` is `'hover'`.
|
|
56
|
+
* @default 200
|
|
57
|
+
*/
|
|
58
|
+
openDelay?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Milliseconds after the pointer leaves the trigger or the panel before it
|
|
61
|
+
* closes. Ignored unless `openOn` is `'hover'`.
|
|
62
|
+
* @default 150
|
|
63
|
+
*/
|
|
64
|
+
closeDelay?: number;
|
|
34
65
|
/**
|
|
35
66
|
* Returns the element the panel should position against.
|
|
36
67
|
* When provided, the panel anchors to this element instead of the trigger —
|
|
37
68
|
* useful when the trigger lives in one place (e.g. a sidebar) and the panel
|
|
38
|
-
* should appear relative to a different reference point.
|
|
69
|
+
* should appear relative to a different reference point. Prefer `DsPopover.Anchor`
|
|
70
|
+
* when the reference is in the tree.
|
|
39
71
|
*/
|
|
40
72
|
getAnchorElement?: () => HTMLElement | null;
|
|
41
73
|
children: ReactNode;
|
|
74
|
+
/**
|
|
75
|
+
* Called when focus is about to move into the panel as it opens.
|
|
76
|
+
* Call `event.preventDefault()` to stop the default move to the first focusable
|
|
77
|
+
* control; focus another node in the same handler if needed.
|
|
78
|
+
*/
|
|
79
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Called when focus is about to leave the panel as it closes.
|
|
82
|
+
* Call `event.preventDefault()` to stop restoring focus to the trigger;
|
|
83
|
+
* focus another node in the same handler if needed.
|
|
84
|
+
* Still runs when `restoreFocus` is false.
|
|
85
|
+
*/
|
|
86
|
+
onCloseAutoFocus?: (event: Event) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Called when the user interacts outside the panel.
|
|
89
|
+
* Call `event.preventDefault()` to keep the panel open. Clicks on
|
|
90
|
+
* `DsPopover.Anchor` already do not dismiss.
|
|
91
|
+
*/
|
|
92
|
+
onInteractOutside?: (event: Event) => void;
|
|
42
93
|
/** Fires when the popover opens or closes. */
|
|
43
94
|
onOpenChange?: (open: boolean) => void;
|
|
44
95
|
}
|
|
@@ -49,6 +100,12 @@ export interface DsPopoverTriggerProps {
|
|
|
49
100
|
className?: string;
|
|
50
101
|
}
|
|
51
102
|
|
|
103
|
+
export interface DsPopoverAnchorProps {
|
|
104
|
+
/** Single element the panel positions against. Wraps the trigger when they share a field. */
|
|
105
|
+
children: ReactNode;
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
52
109
|
export interface DsPopoverPanelProps {
|
|
53
110
|
/**
|
|
54
111
|
* Panel width in pixels.
|
|
@@ -59,6 +116,7 @@ export interface DsPopoverPanelProps {
|
|
|
59
116
|
style?: CSSProperties;
|
|
60
117
|
children: ReactNode;
|
|
61
118
|
ref?: Ref<HTMLDivElement>;
|
|
119
|
+
id?: string;
|
|
62
120
|
'aria-label'?: string;
|
|
63
121
|
}
|
|
64
122
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DsPopoverAlign, DsPopoverSide } from './ds-popover.types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Collapses a side plus alignment into the single placement string Ark expects.
|
|
5
|
+
* `center` is the implicit alignment, so it carries no suffix.
|
|
6
|
+
*/
|
|
7
|
+
export const toPlacement = (side: DsPopoverSide, align: DsPopoverAlign) =>
|
|
8
|
+
align === 'center' ? side : (`${side}-${align}` as const);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether a pointer can hover at all. Touch fires enter/leave around a tap, which
|
|
12
|
+
* would open and immediately re-toggle the panel — on those the click owns opening.
|
|
13
|
+
*/
|
|
14
|
+
export const isHoverPointer = (pointerType: string) => pointerType !== 'touch';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adapts a cancelable DS focus handler into Ark's `initialFocusEl` / `finalFocusEl`.
|
|
18
|
+
* `preventDefault` keeps the current focus; otherwise Ark picks the default target.
|
|
19
|
+
*/
|
|
20
|
+
export const toFocusEl = (handler: ((event: Event) => void) | undefined, eventName: string) =>
|
|
21
|
+
handler
|
|
22
|
+
? () => {
|
|
23
|
+
const event = new Event(eventName, { cancelable: true });
|
|
24
|
+
handler(event);
|
|
25
|
+
|
|
26
|
+
return event.defaultPrevented ? (document.activeElement as HTMLElement | null) : undefined;
|
|
27
|
+
}
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Body and the document element dump keyboard users at the top of the page,
|
|
32
|
+
* so they are not useful restore targets.
|
|
33
|
+
*/
|
|
34
|
+
const isRestorableFocusTarget = (el: EventTarget | null): el is HTMLElement =>
|
|
35
|
+
el instanceof HTMLElement && el.isConnected && el !== document.body && el !== document.documentElement;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runs `onCloseAutoFocus` when Ark's restore is disabled. If the handler
|
|
39
|
+
* prevents default, focus stays on the element that had it at close time.
|
|
40
|
+
*/
|
|
41
|
+
export const invokeCloseAutoFocus = (handler: (event: Event) => void) => {
|
|
42
|
+
const event = new Event('closeAutoFocus', { cancelable: true });
|
|
43
|
+
handler(event);
|
|
44
|
+
|
|
45
|
+
if (!event.defaultPrevented) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const focused = document.activeElement;
|
|
50
|
+
|
|
51
|
+
if (!isRestorableFocusTarget(focused)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
requestAnimationFrame(() => {
|
|
56
|
+
if (focused.isConnected) {
|
|
57
|
+
focused.focus({ preventScroll: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { isHoverPointer, toPlacement } from './ds-popover.utils';
|
|
3
|
+
|
|
4
|
+
describe('toPlacement', () => {
|
|
5
|
+
it('joins side and alignment', () => {
|
|
6
|
+
expect(toPlacement('right', 'start')).toBe('right-start');
|
|
7
|
+
expect(toPlacement('top', 'end')).toBe('top-end');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('drops the suffix for the implicit center alignment', () => {
|
|
11
|
+
expect(toPlacement('bottom', 'center')).toBe('bottom');
|
|
12
|
+
expect(toPlacement('left', 'center')).toBe('left');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('isHoverPointer', () => {
|
|
17
|
+
it('accepts pointers that can hover', () => {
|
|
18
|
+
expect(isHoverPointer('mouse')).toBe(true);
|
|
19
|
+
expect(isHoverPointer('pen')).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('rejects touch, so a tap stays a plain click', () => {
|
|
23
|
+
expect(isHoverPointer('touch')).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DsButtonV3 } from '../../ds-button-v3';
|
|
2
|
+
import { DsModal } from '../../ds-modal';
|
|
3
|
+
import { DsTypography } from '../../ds-typography';
|
|
4
|
+
import {
|
|
5
|
+
interpolateName,
|
|
6
|
+
NAME_MODAL_COLUMNS,
|
|
7
|
+
type Locale,
|
|
8
|
+
type NamedTarget,
|
|
9
|
+
} from '../ds-saved-filters.utils';
|
|
10
|
+
|
|
11
|
+
export const DeleteConfirm = ({
|
|
12
|
+
target,
|
|
13
|
+
locale,
|
|
14
|
+
loading,
|
|
15
|
+
onConfirm,
|
|
16
|
+
onClose,
|
|
17
|
+
}: {
|
|
18
|
+
target: NamedTarget;
|
|
19
|
+
locale: Locale;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
onConfirm: () => void;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
}) => (
|
|
24
|
+
<DsModal
|
|
25
|
+
open
|
|
26
|
+
columns={NAME_MODAL_COLUMNS}
|
|
27
|
+
onOpenChange={(next) => {
|
|
28
|
+
if (!next && !loading) {
|
|
29
|
+
onClose();
|
|
30
|
+
}
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<DsModal.Header>
|
|
34
|
+
<DsModal.Title>{locale.deleteTitle}</DsModal.Title>
|
|
35
|
+
<DsModal.CloseTrigger />
|
|
36
|
+
</DsModal.Header>
|
|
37
|
+
<DsModal.Body>
|
|
38
|
+
<DsTypography variant="body-md-reg">{interpolateName(locale.deleteMessage, target.name)}</DsTypography>
|
|
39
|
+
</DsModal.Body>
|
|
40
|
+
<DsModal.Footer>
|
|
41
|
+
<DsModal.Actions>
|
|
42
|
+
<DsButtonV3 variant="secondary" size="small" disabled={loading} onClick={onClose}>
|
|
43
|
+
{locale.cancel}
|
|
44
|
+
</DsButtonV3>
|
|
45
|
+
<DsButtonV3 color="error" size="small" loading={loading} onClick={onConfirm}>
|
|
46
|
+
{locale.deleteConfirm}
|
|
47
|
+
</DsButtonV3>
|
|
48
|
+
</DsModal.Actions>
|
|
49
|
+
</DsModal.Footer>
|
|
50
|
+
</DsModal>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
DeleteConfirm.displayName = 'DeleteConfirm';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { DsButtonV3 } from '../../ds-button-v3';
|
|
3
|
+
import { DsFormControl } from '../../ds-form-control';
|
|
4
|
+
import { DsStack } from '../../ds-stack';
|
|
5
|
+
|
|
6
|
+
interface NameEntryFieldsProps {
|
|
7
|
+
initialName?: string;
|
|
8
|
+
nameLabel: string;
|
|
9
|
+
confirmLabel: string;
|
|
10
|
+
cancelLabel: string;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
onSubmit: (name: string) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NameEntryFields = ({
|
|
17
|
+
initialName = '',
|
|
18
|
+
nameLabel,
|
|
19
|
+
confirmLabel,
|
|
20
|
+
cancelLabel,
|
|
21
|
+
loading = false,
|
|
22
|
+
onSubmit,
|
|
23
|
+
onCancel,
|
|
24
|
+
}: NameEntryFieldsProps) => {
|
|
25
|
+
const [name, setName] = useState(initialName);
|
|
26
|
+
const trimmed = name.trim();
|
|
27
|
+
const confirmDisabled = trimmed.length === 0;
|
|
28
|
+
|
|
29
|
+
const handleSubmit = () => {
|
|
30
|
+
if (confirmDisabled || loading) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
onSubmit(trimmed);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DsStack direction="column" gap="var(--sm)">
|
|
39
|
+
<DsFormControl label={nameLabel}>
|
|
40
|
+
<DsFormControl.TextInput
|
|
41
|
+
value={name}
|
|
42
|
+
disabled={loading}
|
|
43
|
+
onValueChange={setName}
|
|
44
|
+
onKeyDown={(event) => {
|
|
45
|
+
if (event.key === 'Enter') {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
handleSubmit();
|
|
48
|
+
}
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
</DsFormControl>
|
|
52
|
+
<DsStack direction="row" gap="var(--xs)" justifyContent="flex-end">
|
|
53
|
+
<DsButtonV3 variant="secondary" size="small" disabled={loading} onClick={onCancel}>
|
|
54
|
+
{cancelLabel}
|
|
55
|
+
</DsButtonV3>
|
|
56
|
+
<DsButtonV3 size="small" disabled={confirmDisabled} loading={loading} onClick={handleSubmit}>
|
|
57
|
+
{confirmLabel}
|
|
58
|
+
</DsButtonV3>
|
|
59
|
+
</DsStack>
|
|
60
|
+
</DsStack>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
NameEntryFields.displayName = 'NameEntryFields';
|