@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,110 @@
|
|
|
1
|
+
@use '../../../styles/typography' as typography;
|
|
2
|
+
|
|
3
|
+
$code-surface-max-height: 220px;
|
|
4
|
+
|
|
5
|
+
// One line of code plus the surface padding, so an empty panel keeps its shape.
|
|
6
|
+
$code-surface-min-height: calc(var(--body-line-height-small) + 2 * var(--standard));
|
|
7
|
+
|
|
8
|
+
@mixin code-surface-layer {
|
|
9
|
+
@include typography.code-sm-reg;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: var(--standard);
|
|
13
|
+
border: 0;
|
|
14
|
+
text-align: left;
|
|
15
|
+
direction: ltr;
|
|
16
|
+
tab-size: 2;
|
|
17
|
+
white-space: pre-wrap;
|
|
18
|
+
overflow-wrap: break-word;
|
|
19
|
+
word-break: normal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.body {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.searchRow {
|
|
28
|
+
display: block;
|
|
29
|
+
order: -1;
|
|
30
|
+
padding: var(--standard);
|
|
31
|
+
border-bottom: 1px solid var(--border);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Scrolling both layers as one element is what keeps the marks glued to the glyphs —
|
|
35
|
+
// neither layer scrolls on its own, so there is nothing to keep in sync.
|
|
36
|
+
.codeScroll {
|
|
37
|
+
max-height: $code-surface-max-height;
|
|
38
|
+
overflow-y: auto;
|
|
39
|
+
overflow-x: hidden;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.codeStack {
|
|
43
|
+
position: relative;
|
|
44
|
+
min-height: $code-surface-min-height;
|
|
45
|
+
|
|
46
|
+
--ds-code-input-match-background: var(--background-warning);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.backdrop {
|
|
50
|
+
@include code-surface-layer;
|
|
51
|
+
position: relative;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
user-select: none;
|
|
54
|
+
color: transparent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.mark {
|
|
58
|
+
color: inherit;
|
|
59
|
+
background: var(--ds-code-input-match-background);
|
|
60
|
+
border-radius: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.editorSlot {
|
|
64
|
+
position: absolute;
|
|
65
|
+
inset: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.editorHost {
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.codeArea {
|
|
74
|
+
@include code-surface-layer;
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
display: block;
|
|
78
|
+
width: 100%;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
background: transparent;
|
|
81
|
+
color: var(--font-main);
|
|
82
|
+
caret-color: var(--font-main);
|
|
83
|
+
|
|
84
|
+
// A user-resized textarea would drift away from the backdrop that sizes it.
|
|
85
|
+
resize: none;
|
|
86
|
+
outline: none;
|
|
87
|
+
|
|
88
|
+
&::selection {
|
|
89
|
+
background: var(--form-control-selection-color, var(--background-secondary-hover));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&:read-only,
|
|
93
|
+
&:disabled {
|
|
94
|
+
color: var(--font-main);
|
|
95
|
+
opacity: 1;
|
|
96
|
+
cursor: default;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.visuallyHidden {
|
|
101
|
+
position: absolute;
|
|
102
|
+
width: 1px;
|
|
103
|
+
height: 1px;
|
|
104
|
+
margin: -1px;
|
|
105
|
+
padding: 0;
|
|
106
|
+
border: 0;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
clip-path: inset(50%);
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Ref, Fragment, useEffect, useState } from 'react';
|
|
2
|
+
import { useHighlight } from '@ark-ui/react/highlight';
|
|
3
|
+
import { DsIcon } from '../../ds-icon';
|
|
4
|
+
import { DsTextInput } from '../../ds-text-input';
|
|
5
|
+
import styles from './ds-code-input-panel.module.scss';
|
|
6
|
+
|
|
7
|
+
export interface DsCodeInputPanelProps {
|
|
8
|
+
value: string;
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
searchPlaceholder: string;
|
|
11
|
+
editorSlotRef: Ref<HTMLDivElement>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DsCodeInputPanel = ({ value, isOpen, searchPlaceholder, editorSlotRef }: DsCodeInputPanelProps) => {
|
|
15
|
+
const [query, setQuery] = useState('');
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isOpen) {
|
|
19
|
+
setQuery('');
|
|
20
|
+
}
|
|
21
|
+
}, [isOpen]);
|
|
22
|
+
|
|
23
|
+
const chunks = useHighlight({ text: value, query, ignoreCase: true, matchAll: true });
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={styles.body}>
|
|
27
|
+
<div className={styles.codeScroll}>
|
|
28
|
+
<div className={styles.codeStack}>
|
|
29
|
+
{isOpen && (
|
|
30
|
+
<div className={styles.backdrop} aria-hidden="true">
|
|
31
|
+
{chunks.map((chunk, index) =>
|
|
32
|
+
chunk.match ? (
|
|
33
|
+
<mark key={index} className={styles.mark}>
|
|
34
|
+
{chunk.text}
|
|
35
|
+
</mark>
|
|
36
|
+
) : (
|
|
37
|
+
<Fragment key={index}>{chunk.text}</Fragment>
|
|
38
|
+
),
|
|
39
|
+
)}
|
|
40
|
+
{'\n'}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<div ref={editorSlotRef} className={styles.editorSlot} />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{isOpen && (
|
|
47
|
+
<label className={styles.searchRow}>
|
|
48
|
+
<span className={styles.visuallyHidden}>{searchPlaceholder}</span>
|
|
49
|
+
<DsTextInput
|
|
50
|
+
value={query}
|
|
51
|
+
placeholder={searchPlaceholder}
|
|
52
|
+
onValueChange={setQuery}
|
|
53
|
+
slots={{ startAdornment: <DsIcon icon="search" size="tiny" aria-hidden /> }}
|
|
54
|
+
/>
|
|
55
|
+
</label>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default DsCodeInputPanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DsCodeInputPanel } from './ds-code-input-panel';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: block;
|
|
3
|
+
width: 100%;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.panel {
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
|
|
9
|
+
&.invalid {
|
|
10
|
+
outline: 1px solid var(--background-error);
|
|
11
|
+
outline-offset: -1px;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.visuallyHidden {
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: 1px;
|
|
18
|
+
height: 1px;
|
|
19
|
+
margin: -1px;
|
|
20
|
+
padding: 0;
|
|
21
|
+
border: 0;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
white-space: nowrap;
|
|
24
|
+
clip-path: inset(50%);
|
|
25
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { type ChangeEvent, type FocusEvent, useEffect, useId, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { DsButtonV3, type ButtonV3Size } from '../ds-button-v3';
|
|
5
|
+
import { DsPopover } from '../ds-popover';
|
|
6
|
+
import { mergeRefs } from '../../utils/merge-refs';
|
|
7
|
+
import { CollapsedCodeField } from './collapsed-code-field';
|
|
8
|
+
import collapsedStyles from './collapsed-code-field/collapsed-code-field.module.scss';
|
|
9
|
+
import { DsCodeInputPanel } from './ds-code-input-panel';
|
|
10
|
+
import panelStyles from './ds-code-input-panel/ds-code-input-panel.module.scss';
|
|
11
|
+
import styles from './ds-code-input.module.scss';
|
|
12
|
+
import type { CodeInputSize, DsCodeInputProps } from './ds-code-input.types';
|
|
13
|
+
import {
|
|
14
|
+
applyTextSelection,
|
|
15
|
+
canonicalizeLf,
|
|
16
|
+
getAdditionalLineCount,
|
|
17
|
+
getLogicalLineCount,
|
|
18
|
+
readTextSelection,
|
|
19
|
+
type TextSelection,
|
|
20
|
+
} from './ds-code-input.utils';
|
|
21
|
+
import { useCollapsedViewport } from './use-collapsed-viewport';
|
|
22
|
+
import { useEditorHost } from './use-editor-host';
|
|
23
|
+
|
|
24
|
+
const toggleSizes: Record<CodeInputSize, ButtonV3Size> = Object.freeze({
|
|
25
|
+
small: 'tiny',
|
|
26
|
+
default: 'small',
|
|
27
|
+
large: 'small',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const OVERLAY_GUTTER_PX = 4;
|
|
31
|
+
|
|
32
|
+
const defaultLocale = Object.freeze({
|
|
33
|
+
expand: 'Expand code editor',
|
|
34
|
+
collapse: 'Collapse code editor',
|
|
35
|
+
searchPlaceholder: 'Search in code',
|
|
36
|
+
codeLabel: 'Code',
|
|
37
|
+
additionalLines: (count: number) => `↵ +${String(count)}`,
|
|
38
|
+
additionalLinesLabel: (count: number) => `${String(count)} additional code lines`,
|
|
39
|
+
linePosition: (current: number, total: number) => `Ln ${String(current)}/${String(total)}`,
|
|
40
|
+
multilineDescription: (total: number) =>
|
|
41
|
+
`Multiline code, ${String(total)} lines. Only one line is visible. Expand to view the complete code.`,
|
|
42
|
+
linePositionDescription: (current: number, total: number) =>
|
|
43
|
+
`Line ${String(current)} of ${String(total)}; other code lines are hidden.`,
|
|
44
|
+
selectionStatus: (selected: number, total: number) =>
|
|
45
|
+
`${String(selected)} lines selected · ${String(total)} total`,
|
|
46
|
+
expandedAnnouncement: (total: number) => `Code editor expanded, ${String(total)} lines.`,
|
|
47
|
+
collapsedAnnouncement: 'Code editor collapsed',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Multiline editor for code, such as a query expression. The collapsed field is a
|
|
52
|
+
* one-row viewport of the same document; expanding opens a larger overlay with search.
|
|
53
|
+
* There is one textarea: collapsed and expanded are two homes it moves between.
|
|
54
|
+
*
|
|
55
|
+
* @summary multiline code field that expands into a searchable overlay
|
|
56
|
+
*/
|
|
57
|
+
const DsCodeInput = ({
|
|
58
|
+
id,
|
|
59
|
+
ref,
|
|
60
|
+
name,
|
|
61
|
+
size = 'default',
|
|
62
|
+
value,
|
|
63
|
+
defaultValue,
|
|
64
|
+
placeholder,
|
|
65
|
+
disabled = false,
|
|
66
|
+
readOnly = false,
|
|
67
|
+
invalid = false,
|
|
68
|
+
'aria-describedby': ariaDescribedBy,
|
|
69
|
+
expanded,
|
|
70
|
+
defaultExpanded = false,
|
|
71
|
+
onExpandChange,
|
|
72
|
+
onChange,
|
|
73
|
+
onValueChange,
|
|
74
|
+
onFocus,
|
|
75
|
+
onBlur,
|
|
76
|
+
maxLength,
|
|
77
|
+
locale,
|
|
78
|
+
slots,
|
|
79
|
+
className,
|
|
80
|
+
style,
|
|
81
|
+
}: DsCodeInputProps) => {
|
|
82
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
|
|
83
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
|
|
84
|
+
// True when the user opened the overlay and should keep typing in it.
|
|
85
|
+
const [focusOnOpen, setFocusOnOpen] = useState(false);
|
|
86
|
+
const [announcement, setAnnouncement] = useState('');
|
|
87
|
+
|
|
88
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
89
|
+
const collapsedSlotRef = useRef<HTMLDivElement>(null);
|
|
90
|
+
const overlaySlotRef = useRef<HTMLDivElement>(null);
|
|
91
|
+
// Caret/selection saved before the textarea moves to the other slot.
|
|
92
|
+
const pendingSelectionRef = useRef<TextSelection | null>(null);
|
|
93
|
+
// True when the user closed the overlay and should land back in the field.
|
|
94
|
+
const focusOnCloseRef = useRef(false);
|
|
95
|
+
// Skip the first expand/collapse announcement so mount is silent.
|
|
96
|
+
const skipAnnouncementRef = useRef(true);
|
|
97
|
+
|
|
98
|
+
const panelId = useId();
|
|
99
|
+
const descriptionId = useId();
|
|
100
|
+
const announcementId = useId();
|
|
101
|
+
|
|
102
|
+
const strings = { ...defaultLocale, ...locale };
|
|
103
|
+
|
|
104
|
+
const isValueControlled = value !== undefined;
|
|
105
|
+
const currentValue = isValueControlled ? value : uncontrolledValue;
|
|
106
|
+
|
|
107
|
+
const isExpandedControlled = expanded !== undefined;
|
|
108
|
+
const isExpanded = isExpandedControlled ? expanded : uncontrolledExpanded;
|
|
109
|
+
|
|
110
|
+
const handleOpenChange = (open: boolean) => {
|
|
111
|
+
if (open) {
|
|
112
|
+
focusOnCloseRef.current = false;
|
|
113
|
+
|
|
114
|
+
// User-open already saved the caret. If the overlay opened some other
|
|
115
|
+
// way, snapshot it now so it still comes back after the move.
|
|
116
|
+
if (!pendingSelectionRef.current) {
|
|
117
|
+
const textarea = textareaRef.current;
|
|
118
|
+
|
|
119
|
+
if (textarea) {
|
|
120
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
setFocusOnOpen(false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isExpandedControlled) {
|
|
128
|
+
setUncontrolledExpanded(open);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onExpandChange?.(open);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const { host, hostReady, isMovingHostRef } = useEditorHost({
|
|
135
|
+
isExpanded,
|
|
136
|
+
collapsedSlotRef,
|
|
137
|
+
overlaySlotRef,
|
|
138
|
+
hostClassName: isExpanded ? panelStyles.editorHost : collapsedStyles.editorHost,
|
|
139
|
+
textareaRef,
|
|
140
|
+
pendingSelectionRef,
|
|
141
|
+
shouldFocus: isExpanded && focusOnOpen,
|
|
142
|
+
});
|
|
143
|
+
const viewport = useCollapsedViewport(textareaRef, currentValue, hostReady, !isExpanded);
|
|
144
|
+
const revealCaretLineRef = useRef(viewport.revealCaretLine);
|
|
145
|
+
|
|
146
|
+
revealCaretLineRef.current = viewport.revealCaretLine;
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (skipAnnouncementRef.current) {
|
|
150
|
+
skipAnnouncementRef.current = false;
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setAnnouncement(
|
|
156
|
+
isExpanded
|
|
157
|
+
? strings.expandedAnnouncement(getLogicalLineCount(currentValue))
|
|
158
|
+
: strings.collapsedAnnouncement,
|
|
159
|
+
);
|
|
160
|
+
// Announce presentation changes only — not every keystroke in the document.
|
|
161
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- currentValue/strings would re-announce while typing
|
|
162
|
+
}, [isExpanded]);
|
|
163
|
+
|
|
164
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
165
|
+
// Some browsers insert \r\n. Rewrite to \n and put the caret back so
|
|
166
|
+
// typing does not jump.
|
|
167
|
+
const newValue = canonicalizeLf(event.target.value);
|
|
168
|
+
|
|
169
|
+
if (newValue !== event.target.value) {
|
|
170
|
+
const selection = readTextSelection(event.target);
|
|
171
|
+
|
|
172
|
+
event.target.value = newValue;
|
|
173
|
+
applyTextSelection(event.target, selection);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!isValueControlled) {
|
|
177
|
+
setUncontrolledValue(newValue);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onChange?.(event);
|
|
181
|
+
onValueChange?.(newValue);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
185
|
+
// Moving the host fires fake focus/blur. Ignore those so callers do not
|
|
186
|
+
// think the user left the field.
|
|
187
|
+
if (isMovingHostRef.current) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onFocus?.(event);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
|
|
195
|
+
if (isMovingHostRef.current) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onBlur?.(event);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleUserOpen = () => {
|
|
203
|
+
const textarea = textareaRef.current;
|
|
204
|
+
|
|
205
|
+
if (textarea) {
|
|
206
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setFocusOnOpen(true);
|
|
210
|
+
handleOpenChange(true);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const additionalLines = getAdditionalLineCount(currentValue);
|
|
214
|
+
const description =
|
|
215
|
+
!isExpanded && additionalLines > 0
|
|
216
|
+
? viewport.focused
|
|
217
|
+
? strings.linePositionDescription(viewport.visibleLine, viewport.totalLines)
|
|
218
|
+
: strings.multilineDescription(viewport.totalLines)
|
|
219
|
+
: undefined;
|
|
220
|
+
|
|
221
|
+
// One textarea, rendered once and portaled into the host. rows / wrap /
|
|
222
|
+
// className flip when it moves between the collapsed slot and the overlay.
|
|
223
|
+
const editor = host
|
|
224
|
+
? createPortal(
|
|
225
|
+
<textarea
|
|
226
|
+
id={id}
|
|
227
|
+
ref={mergeRefs(ref, textareaRef)}
|
|
228
|
+
name={name}
|
|
229
|
+
className={isExpanded ? panelStyles.codeArea : collapsedStyles.input}
|
|
230
|
+
rows={isExpanded ? undefined : 1}
|
|
231
|
+
wrap={isExpanded ? 'soft' : 'off'}
|
|
232
|
+
value={currentValue}
|
|
233
|
+
placeholder={placeholder}
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
readOnly={readOnly}
|
|
236
|
+
maxLength={maxLength}
|
|
237
|
+
spellCheck={false}
|
|
238
|
+
autoComplete="off"
|
|
239
|
+
aria-multiline="true"
|
|
240
|
+
aria-label={isExpanded && !id ? strings.codeLabel : undefined}
|
|
241
|
+
aria-invalid={invalid || undefined}
|
|
242
|
+
aria-describedby={
|
|
243
|
+
[description && descriptionId, ariaDescribedBy].filter(Boolean).join(' ') || undefined
|
|
244
|
+
}
|
|
245
|
+
onChange={handleChange}
|
|
246
|
+
onSelect={viewport.onSelect}
|
|
247
|
+
onKeyUp={viewport.onKeyUp}
|
|
248
|
+
onClick={viewport.onClick}
|
|
249
|
+
onScroll={viewport.onScroll}
|
|
250
|
+
onBeforeInput={viewport.onBeforeInput}
|
|
251
|
+
onFocus={(event) => {
|
|
252
|
+
if (!isMovingHostRef.current) {
|
|
253
|
+
viewport.onFocus();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
handleFocus(event);
|
|
257
|
+
}}
|
|
258
|
+
onBlur={(event) => {
|
|
259
|
+
if (!isMovingHostRef.current) {
|
|
260
|
+
viewport.onBlur();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handleBlur(event);
|
|
264
|
+
}}
|
|
265
|
+
onKeyDown={(event) => {
|
|
266
|
+
if (event.key === 'Escape' && isExpanded) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
focusOnCloseRef.current = true;
|
|
269
|
+
handleOpenChange(false);
|
|
270
|
+
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!isExpanded && event.altKey && event.key === 'ArrowDown') {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
event.stopPropagation();
|
|
277
|
+
handleUserOpen();
|
|
278
|
+
}
|
|
279
|
+
}}
|
|
280
|
+
/>,
|
|
281
|
+
host,
|
|
282
|
+
)
|
|
283
|
+
: null;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<DsPopover.Root
|
|
287
|
+
open={isExpanded}
|
|
288
|
+
side="bottom"
|
|
289
|
+
align="start"
|
|
290
|
+
gutter={OVERLAY_GUTTER_PX}
|
|
291
|
+
matchAnchorWidth
|
|
292
|
+
// We own focus so the expand button does not steal it.
|
|
293
|
+
restoreFocus={false}
|
|
294
|
+
onOpenAutoFocus={(event) => {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
|
|
297
|
+
if (focusOnOpen) {
|
|
298
|
+
textareaRef.current?.focus({ preventScroll: true });
|
|
299
|
+
}
|
|
300
|
+
}}
|
|
301
|
+
onCloseAutoFocus={(event) => {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
|
|
304
|
+
// Put focus back in the collapsed field and scroll to the caret line.
|
|
305
|
+
if (focusOnCloseRef.current) {
|
|
306
|
+
textareaRef.current?.focus({ preventScroll: true });
|
|
307
|
+
revealCaretLineRef.current();
|
|
308
|
+
focusOnCloseRef.current = false;
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
onOpenChange={handleOpenChange}
|
|
312
|
+
>
|
|
313
|
+
<DsPopover.Anchor>
|
|
314
|
+
<div className={classNames(styles.root, className)} style={style}>
|
|
315
|
+
<CollapsedCodeField
|
|
316
|
+
size={size}
|
|
317
|
+
placeholder={placeholder}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
invalid={invalid}
|
|
320
|
+
showEcho={isExpanded}
|
|
321
|
+
echoText={viewport.echoText}
|
|
322
|
+
fadeStart={viewport.fadeStart}
|
|
323
|
+
fadeEnd={viewport.fadeEnd}
|
|
324
|
+
showEllipsis={viewport.showEllipsis}
|
|
325
|
+
indicator={
|
|
326
|
+
isExpanded && additionalLines > 0
|
|
327
|
+
? { kind: 'line', current: viewport.caretLine, total: viewport.totalLines }
|
|
328
|
+
: isExpanded
|
|
329
|
+
? null
|
|
330
|
+
: viewport.indicator
|
|
331
|
+
}
|
|
332
|
+
additionalLinesLabel={strings.additionalLinesLabel}
|
|
333
|
+
additionalLines={strings.additionalLines}
|
|
334
|
+
linePosition={strings.linePosition}
|
|
335
|
+
selectionStatus={strings.selectionStatus}
|
|
336
|
+
editorSlotRef={collapsedSlotRef}
|
|
337
|
+
onExpand={handleUserOpen}
|
|
338
|
+
adornment={
|
|
339
|
+
<>
|
|
340
|
+
<DsPopover.Trigger>
|
|
341
|
+
<DsButtonV3
|
|
342
|
+
variant="tertiary"
|
|
343
|
+
size={toggleSizes[size]}
|
|
344
|
+
icon={isExpanded ? 'collapse_content' : 'expand_content'}
|
|
345
|
+
aria-label={isExpanded ? strings.collapse : strings.expand}
|
|
346
|
+
aria-expanded={isExpanded}
|
|
347
|
+
aria-controls={panelId}
|
|
348
|
+
onPointerDown={(event) => {
|
|
349
|
+
// Snapshot the caret and request focus before the
|
|
350
|
+
// button would steal it.
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
|
|
353
|
+
if (isExpanded) {
|
|
354
|
+
focusOnCloseRef.current = true;
|
|
355
|
+
} else {
|
|
356
|
+
const textarea = textareaRef.current;
|
|
357
|
+
|
|
358
|
+
if (textarea) {
|
|
359
|
+
pendingSelectionRef.current = readTextSelection(textarea);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setFocusOnOpen(true);
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
</DsPopover.Trigger>
|
|
367
|
+
{slots?.endAdornment}
|
|
368
|
+
</>
|
|
369
|
+
}
|
|
370
|
+
/>
|
|
371
|
+
{description && (
|
|
372
|
+
<span id={descriptionId} className={styles.visuallyHidden}>
|
|
373
|
+
{description}
|
|
374
|
+
</span>
|
|
375
|
+
)}
|
|
376
|
+
<div id={announcementId} className={styles.visuallyHidden} aria-live="polite" aria-atomic="true">
|
|
377
|
+
{announcement}
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</DsPopover.Anchor>
|
|
381
|
+
|
|
382
|
+
<DsPopover.Panel
|
|
383
|
+
id={panelId}
|
|
384
|
+
className={classNames(styles.panel, invalid && styles.invalid)}
|
|
385
|
+
aria-label={strings.codeLabel}
|
|
386
|
+
>
|
|
387
|
+
<DsCodeInputPanel
|
|
388
|
+
value={currentValue}
|
|
389
|
+
isOpen={isExpanded}
|
|
390
|
+
searchPlaceholder={strings.searchPlaceholder}
|
|
391
|
+
editorSlotRef={overlaySlotRef}
|
|
392
|
+
/>
|
|
393
|
+
</DsPopover.Panel>
|
|
394
|
+
{editor}
|
|
395
|
+
</DsPopover.Root>
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
DsCodeInput.displayName = 'DsCodeInput';
|
|
400
|
+
|
|
401
|
+
export default DsCodeInput;
|