@dbcdk/react-components 0.0.7 → 0.0.9
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/README.md +167 -0
- package/dist/components/__stories__/_data/tabs.d.ts +9 -0
- package/dist/components/__stories__/_data/tabs.js +31 -0
- package/dist/components/code-block/CodeBlock.module.css +1 -1
- package/dist/components/datetime-picker/DateTimePicker.d.ts +4 -3
- package/dist/components/datetime-picker/DateTimePicker.js +123 -50
- package/dist/components/datetime-picker/dateTimeHelpers.d.ts +11 -1
- package/dist/components/datetime-picker/dateTimeHelpers.js +44 -9
- package/dist/components/forms/select/Select.d.ts +2 -1
- package/dist/components/forms/select/Select.js +3 -8
- package/dist/components/overlay/modal/provider/ModalProvider.d.ts +2 -2
- package/dist/components/overlay/modal/provider/ModalProvider.js +24 -25
- package/dist/components/popover/Popover.js +33 -14
- package/dist/components/popover/Popover.module.css +0 -4
- package/dist/components/table/tanstack.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# DBC React Components
|
|
2
|
+
|
|
3
|
+
Reusable React components for DBC projects.
|
|
4
|
+
|
|
5
|
+
This library provides a shared, themeable component system used across DBC’s internal (and selected external) applications. It is designed to promote consistency, speed up development, and improve overall quality and accessibility.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Purpose of the library
|
|
10
|
+
|
|
11
|
+
The goals of this component library are:
|
|
12
|
+
|
|
13
|
+
- **Consistency**
|
|
14
|
+
Provide a unified look and feel across DBC’s digital products, especially internal tools.
|
|
15
|
+
|
|
16
|
+
- **Development speed**
|
|
17
|
+
Reduce development and maintenance time by reusing well-tested components instead of rebuilding UI from scratch.
|
|
18
|
+
|
|
19
|
+
- **Quality & accessibility**
|
|
20
|
+
Components are reviewed and built according to best practices, with accessibility and robustness in mind, ensuring a strong baseline quality.
|
|
21
|
+
|
|
22
|
+
- **Reduced third-party dependency**
|
|
23
|
+
Increase digital independence by building and sharing our own components across the organisation, reducing reliance on external NPM packages.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Getting started
|
|
28
|
+
|
|
29
|
+
### 1) Install the package
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @dbcdk/react-components
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### 2) Import global styles
|
|
38
|
+
|
|
39
|
+
> **Important:** The component library requires global styles to be imported once in your application.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import '@dbcdk/react-components/styles.css'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 3) Add the theme `<link>` in your root layout (Next.js example)
|
|
48
|
+
|
|
49
|
+
The library uses theme CSS files that are dynamically loaded via a `<link>` tag in `<head>`.
|
|
50
|
+
You **must** use the exported `LINK_ID` so the `useTheme()` hook can update the active theme at runtime.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { ReactNode } from 'react'
|
|
54
|
+
import { cookies } from 'next/headers'
|
|
55
|
+
|
|
56
|
+
import { LINK_ID } from '@dbcdk/react-components'
|
|
57
|
+
import '@dbcdk/react-components/styles.css'
|
|
58
|
+
|
|
59
|
+
export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
|
60
|
+
const cookieStore = await cookies()
|
|
61
|
+
const themeId = cookieStore.get('dbc_theme')?.value || 'light'
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<html lang="da">
|
|
65
|
+
<head>
|
|
66
|
+
<link id={LINK_ID} rel="stylesheet" href={`/themes/${themeId}.css`} />
|
|
67
|
+
</head>
|
|
68
|
+
<body>{children}</body>
|
|
69
|
+
</html>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> Theme files are expected to be served from `/themes/<theme>.css`.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### 4) Switching theme in your application
|
|
79
|
+
|
|
80
|
+
Example using `useTheme()`:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
'use client'
|
|
84
|
+
|
|
85
|
+
import { AppHeader, Button, useTheme } from '@dbcdk/react-components'
|
|
86
|
+
import { Moon, Sun } from 'lucide-react'
|
|
87
|
+
|
|
88
|
+
export default function Header() {
|
|
89
|
+
const { theme, switchTheme } = useTheme()
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<AppHeader>
|
|
93
|
+
<Button variant="outlined" onClick={() => switchTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
94
|
+
{theme === 'light' ? <Moon /> : <Sun />}
|
|
95
|
+
</Button>
|
|
96
|
+
</AppHeader>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The hook updates the `<link>` tag automatically and persists the selected theme.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Using Storybook
|
|
106
|
+
|
|
107
|
+
Storybook is the primary documentation and exploration tool for this library.
|
|
108
|
+
|
|
109
|
+
### Local Storybook
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run storybook
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Storybook runs on `http://localhost:6006`.
|
|
116
|
+
|
|
117
|
+
### In Storybook you can:
|
|
118
|
+
|
|
119
|
+
1. Browse components in the left-hand navigation
|
|
120
|
+
2. Open a story to see variants and states
|
|
121
|
+
3. Adjust props via **Controls**
|
|
122
|
+
4. Read guidelines and usage notes in the **Docs** tab
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Themes
|
|
127
|
+
|
|
128
|
+
Use the 🎨 **theme selector** in the Storybook toolbar to switch between available themes (e.g. light and dark).
|
|
129
|
+
|
|
130
|
+
All components are styled using CSS variables defined in the theme files.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Accessibility (a11y)
|
|
135
|
+
|
|
136
|
+
Accessibility is a first-class concern in this library.
|
|
137
|
+
|
|
138
|
+
We aim to ensure that components:
|
|
139
|
+
|
|
140
|
+
- Are usable with keyboard navigation
|
|
141
|
+
- Have visible and consistent focus states
|
|
142
|
+
- Work with screen readers
|
|
143
|
+
- Follow common ARIA and semantic HTML best practices
|
|
144
|
+
|
|
145
|
+
Storybook includes the a11y addon to help identify issues during development.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Contributing
|
|
150
|
+
|
|
151
|
+
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for detailed guidelines on:
|
|
152
|
+
|
|
153
|
+
- Folder structure
|
|
154
|
+
- Styling and theming rules
|
|
155
|
+
- Storybook requirements
|
|
156
|
+
- TypeScript conventions
|
|
157
|
+
- Versioning and changesets
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
ISC
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { TabItem } from '../../../components/tabs/Tabs';
|
|
3
|
+
export declare const tabItems: TabItem[];
|
|
4
|
+
export declare const tabArgs: {
|
|
5
|
+
tabs: TabItem[];
|
|
6
|
+
variant: 'filled' | 'outlined';
|
|
7
|
+
header: string;
|
|
8
|
+
addition: ReactNode;
|
|
9
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Hourglass, Undo2 } from 'lucide-react';
|
|
3
|
+
import { Button } from '../../../components/button/Button';
|
|
4
|
+
import { Icon } from '../../../components/icon/Icon';
|
|
5
|
+
import { SampleTable } from './table';
|
|
6
|
+
export const tabItems = [
|
|
7
|
+
{
|
|
8
|
+
header: 'Afvist af anmelder',
|
|
9
|
+
id: 1,
|
|
10
|
+
headerIcon: _jsx(Icon, { severity: "error" }),
|
|
11
|
+
content: _jsx(SampleTable, {}),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
header: 'Afventer godkendelse',
|
|
15
|
+
id: 2,
|
|
16
|
+
headerIcon: _jsx(Hourglass, {}),
|
|
17
|
+
content: _jsx(SampleTable, {}),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
header: 'Retur til redigering',
|
|
21
|
+
id: 3,
|
|
22
|
+
headerIcon: _jsx(Undo2, {}),
|
|
23
|
+
content: _jsx(SampleTable, {}),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
export const tabArgs = {
|
|
27
|
+
tabs: tabItems,
|
|
28
|
+
variant: 'filled',
|
|
29
|
+
header: 'Sagsoversigt',
|
|
30
|
+
addition: _jsx(Button, { children: "Se historik" }),
|
|
31
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input } from '../../components/forms/input/Input';
|
|
3
|
+
import { DateOnly } from './dateTimeHelpers';
|
|
3
4
|
type Mode = 'single' | 'range';
|
|
4
5
|
type WeekStart = 0 | 1;
|
|
5
|
-
export type DateValue =
|
|
6
|
-
start:
|
|
7
|
-
end:
|
|
6
|
+
export type DateValue = number | DateOnly | null | {
|
|
7
|
+
start: DateOnly | null;
|
|
8
|
+
end: DateOnly | null;
|
|
8
9
|
};
|
|
9
10
|
type InputProps = React.ComponentProps<typeof Input>;
|
|
10
11
|
export interface DateTimePickerProps {
|
|
@@ -5,8 +5,8 @@ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'r
|
|
|
5
5
|
import { Button } from '../../components/button/Button';
|
|
6
6
|
import { Input } from '../../components/forms/input/Input';
|
|
7
7
|
import { Popover } from '../../components/popover/Popover';
|
|
8
|
+
import { localDateFromYMD, maskRange, maskSingle, pad2, parseLooseDateOrDateTime, parseLooseRange, toMaskedFromDate, toMaskedFromYMD, utcMillisFromYMD, ymdFromLocalDate, ymdFromUTCDateOnly, } from './dateTimeHelpers';
|
|
8
9
|
import styles from './DateTimePicker.module.css';
|
|
9
|
-
import { maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange, toMaskedFromDate, toMaskedRange, } from './dateTimeHelpers';
|
|
10
10
|
/* ---------- Date grid helpers (UTC) ---------- */
|
|
11
11
|
const dUTC = (y, m, day) => new Date(Date.UTC(y, m, day));
|
|
12
12
|
const addDaysUTC = (utcDate, n) => dUTC(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate() + n);
|
|
@@ -33,13 +33,6 @@ const isBetweenUTC = (d, a, b) => {
|
|
|
33
33
|
const t = +d, s = +a, e = +b;
|
|
34
34
|
return t >= Math.min(s, e) && t <= Math.max(s, e);
|
|
35
35
|
};
|
|
36
|
-
function composeLocalDateTimeISO(utcDateOnly, hh, mm) {
|
|
37
|
-
const y = utcDateOnly.getUTCFullYear();
|
|
38
|
-
const m = utcDateOnly.getUTCMonth();
|
|
39
|
-
const d = utcDateOnly.getUTCDate();
|
|
40
|
-
const local = new Date(y, m, d, hh, mm, 0, 0);
|
|
41
|
-
return local.toISOString();
|
|
42
|
-
}
|
|
43
36
|
/* ---------- Formatting (exposed but input uses mask) ---------- */
|
|
44
37
|
function defaultFormatDate(d, { locale, enableTime }) {
|
|
45
38
|
const opts = enableTime
|
|
@@ -57,37 +50,79 @@ function defaultFormatRange(s, e, opts) {
|
|
|
57
50
|
return '';
|
|
58
51
|
}
|
|
59
52
|
const cx = (...classes) => classes.filter(Boolean).join(' ');
|
|
60
|
-
export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate,
|
|
61
|
-
formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
62
|
-
}, _ref) {
|
|
53
|
+
export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate, formatRange = defaultFormatRange, }, _ref) {
|
|
63
54
|
void formatDate;
|
|
64
55
|
void formatRange;
|
|
65
56
|
const popRef = useRef(null);
|
|
66
57
|
const todayLocal = useMemo(() => new Date(), []);
|
|
58
|
+
// ---- Derive a local anchor from the controlled value ----
|
|
67
59
|
const initialAnchor = useMemo(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
var _a, _b;
|
|
61
|
+
if (mode === 'single') {
|
|
62
|
+
if (enableTime && typeof value === 'number')
|
|
63
|
+
return new Date(value); // local rendering
|
|
64
|
+
if (!enableTime && typeof value === 'string')
|
|
65
|
+
return (_a = localDateFromYMD(value)) !== null && _a !== void 0 ? _a : todayLocal;
|
|
66
|
+
return todayLocal;
|
|
67
|
+
}
|
|
68
|
+
if (mode === 'range' && value && typeof value === 'object' && 'start' in value && value.start) {
|
|
69
|
+
return (_b = localDateFromYMD(value.start)) !== null && _b !== void 0 ? _b : todayLocal;
|
|
70
|
+
}
|
|
72
71
|
return todayLocal;
|
|
73
|
-
}, [mode, value, todayLocal]);
|
|
72
|
+
}, [mode, value, enableTime, todayLocal]);
|
|
74
73
|
const [monthAnchor, setMonthAnchor] = useState(initialAnchor);
|
|
74
|
+
// Keep month anchor in sync when external value changes (but don’t fight while typing)
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setMonthAnchor(initialAnchor);
|
|
77
|
+
}, [initialAnchor]);
|
|
75
78
|
const [timeHH, setTimeHH] = useState(todayLocal.getHours());
|
|
76
79
|
const [timeMM, setTimeMM] = useState(Math.floor(todayLocal.getMinutes() / timeStep) * timeStep);
|
|
80
|
+
// If value is a datetime and changes externally, keep dropdowns in sync
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (mode === 'single' && enableTime && typeof value === 'number') {
|
|
83
|
+
const d = new Date(value);
|
|
84
|
+
setTimeHH(d.getHours());
|
|
85
|
+
setTimeMM(Math.floor(d.getMinutes() / timeStep) * timeStep);
|
|
86
|
+
}
|
|
87
|
+
}, [mode, enableTime, value, timeStep]);
|
|
77
88
|
const [hoverUTC, setHoverUTC] = useState(null);
|
|
78
89
|
const cellsUTC = useMemo(() => buildMonthGrid(monthAnchor, weekStartsOn), [monthAnchor, weekStartsOn]);
|
|
79
90
|
const monthStartUTC = useMemo(() => startOfMonthUTC(toUTCDateOnly(monthAnchor)), [monthAnchor]);
|
|
80
91
|
const monthEndUTC = useMemo(() => endOfMonthUTC(toUTCDateOnly(monthAnchor)), [monthAnchor]);
|
|
81
92
|
const weekdayFmt = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: 'short' }), [locale]);
|
|
82
93
|
const monthFmt = useMemo(() => new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }), [locale]);
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
// ---- Selection state for the grid (always compared in UTC date-only space) ----
|
|
95
|
+
const selectedUTC_single = useMemo(() => {
|
|
96
|
+
if (mode !== 'single' || !value)
|
|
97
|
+
return null;
|
|
98
|
+
if (enableTime) {
|
|
99
|
+
if (typeof value !== 'number')
|
|
100
|
+
return null;
|
|
101
|
+
return toUTCDateOnly(new Date(value));
|
|
102
|
+
}
|
|
103
|
+
if (typeof value !== 'string')
|
|
104
|
+
return null;
|
|
105
|
+
const local = localDateFromYMD(value);
|
|
106
|
+
return local ? toUTCDateOnly(local) : null;
|
|
107
|
+
}, [mode, value, enableTime]);
|
|
108
|
+
const selectedUTC_start = useMemo(() => {
|
|
109
|
+
if (mode !== 'range' || !value || typeof value !== 'object' || !('start' in value))
|
|
110
|
+
return null;
|
|
111
|
+
if (!value.start)
|
|
112
|
+
return null;
|
|
113
|
+
const local = localDateFromYMD(value.start);
|
|
114
|
+
return local ? toUTCDateOnly(local) : null;
|
|
115
|
+
}, [mode, value]);
|
|
116
|
+
const selectedUTC_end = useMemo(() => {
|
|
117
|
+
if (mode !== 'range' || !value || typeof value !== 'object' || !('end' in value))
|
|
118
|
+
return null;
|
|
119
|
+
if (!value.end)
|
|
120
|
+
return null;
|
|
121
|
+
const local = localDateFromYMD(value.end);
|
|
122
|
+
return local ? toUTCDateOnly(local) : null;
|
|
123
|
+
}, [mode, value]);
|
|
90
124
|
const isDisabledUTC = useCallback((utcDay) => {
|
|
125
|
+
// min/max are Dates (instants). Treat them as local-day constraints for UI.
|
|
91
126
|
if (min && utcDay < toUTCDateOnly(min))
|
|
92
127
|
return true;
|
|
93
128
|
if (max && utcDay > toUTCDateOnly(max))
|
|
@@ -100,27 +135,35 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
|
100
135
|
return;
|
|
101
136
|
if (mode === 'single') {
|
|
102
137
|
if (enableTime) {
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
// User picked a local wall time; emit UTC instant as millis
|
|
139
|
+
const y = utcDay.getUTCFullYear();
|
|
140
|
+
const m = utcDay.getUTCMonth();
|
|
141
|
+
const d = utcDay.getUTCDate();
|
|
142
|
+
const local = new Date(y, m, d, timeHH, timeMM, 0, 0);
|
|
143
|
+
onChange(local.getTime());
|
|
105
144
|
}
|
|
106
145
|
else {
|
|
107
|
-
|
|
146
|
+
// Date-only: emit timezone-free day label
|
|
147
|
+
onChange(ymdFromUTCDateOnly(utcDay));
|
|
108
148
|
}
|
|
109
149
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
110
150
|
return;
|
|
111
151
|
}
|
|
112
|
-
|
|
152
|
+
// RANGE: date-only
|
|
153
|
+
const curr = value && typeof value === 'object' && 'start' in value
|
|
154
|
+
? value
|
|
155
|
+
: { start: null, end: null };
|
|
156
|
+
const picked = ymdFromUTCDateOnly(utcDay);
|
|
113
157
|
if (!curr.start || (curr.start && curr.end)) {
|
|
114
|
-
onChange({ start:
|
|
115
|
-
|
|
116
|
-
else {
|
|
117
|
-
const startUTC = toUTCDateOnly(curr.start);
|
|
118
|
-
const endUTC = utcDay;
|
|
119
|
-
const s = new Date(Math.min(+startUTC, +endUTC));
|
|
120
|
-
const e = new Date(Math.max(+startUTC, +endUTC));
|
|
121
|
-
onChange({ start: s, end: e });
|
|
122
|
-
(_b = popRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
158
|
+
onChange({ start: picked, end: null });
|
|
159
|
+
return;
|
|
123
160
|
}
|
|
161
|
+
const a = utcMillisFromYMD(curr.start);
|
|
162
|
+
const b = utcMillisFromYMD(picked);
|
|
163
|
+
const start = a <= b ? curr.start : picked;
|
|
164
|
+
const end = a <= b ? picked : curr.start;
|
|
165
|
+
onChange({ start, end });
|
|
166
|
+
(_b = popRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
124
167
|
};
|
|
125
168
|
const gridRef = useRef(null);
|
|
126
169
|
useEffect(() => {
|
|
@@ -177,20 +220,37 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
|
177
220
|
}, [monthAnchor]);
|
|
178
221
|
const hours = useMemo(() => Array.from({ length: 24 }, (_, i) => i), []);
|
|
179
222
|
const minutes = useMemo(() => Array.from({ length: Math.floor(60 / (timeStep || 1)) }, (_, i) => i * (timeStep || 1)), [timeStep]);
|
|
180
|
-
// ---- Input display:
|
|
223
|
+
// ---- Input display: always local ----
|
|
181
224
|
const formatted = useMemo(() => {
|
|
182
|
-
var _a, _b;
|
|
183
225
|
if (mode === 'single') {
|
|
184
|
-
|
|
226
|
+
if (!value)
|
|
227
|
+
return '';
|
|
228
|
+
if (enableTime) {
|
|
229
|
+
if (typeof value !== 'number')
|
|
230
|
+
return '';
|
|
231
|
+
return toMaskedFromDate(new Date(value), true);
|
|
232
|
+
}
|
|
233
|
+
if (typeof value !== 'string')
|
|
234
|
+
return '';
|
|
235
|
+
return toMaskedFromYMD(value);
|
|
185
236
|
}
|
|
237
|
+
// range (date-only)
|
|
186
238
|
const v = value;
|
|
187
|
-
|
|
239
|
+
const s = typeof (v === null || v === void 0 ? void 0 : v.start) === 'string' ? toMaskedFromYMD(v.start) : '';
|
|
240
|
+
const e = typeof (v === null || v === void 0 ? void 0 : v.end) === 'string' ? toMaskedFromYMD(v.end) : '';
|
|
241
|
+
if (s && e)
|
|
242
|
+
return `${s} – ${e}`;
|
|
243
|
+
if (s)
|
|
244
|
+
return `${s} –`;
|
|
245
|
+
if (e)
|
|
246
|
+
return `– ${e}`;
|
|
247
|
+
return '';
|
|
188
248
|
}, [mode, value, enableTime]);
|
|
189
249
|
const [text, setText] = useState(formatted);
|
|
190
250
|
const [dirty, setDirty] = useState(false); // while user is typing
|
|
191
251
|
useEffect(() => {
|
|
192
252
|
if (!dirty)
|
|
193
|
-
setText(formatted);
|
|
253
|
+
setText(formatted);
|
|
194
254
|
}, [formatted, dirty]);
|
|
195
255
|
const commitTypedValue = useCallback(() => {
|
|
196
256
|
if (!text.trim()) {
|
|
@@ -203,20 +263,30 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
|
203
263
|
}
|
|
204
264
|
if (mode === 'single') {
|
|
205
265
|
const dLocal = parseLooseDateOrDateTime(text);
|
|
206
|
-
if (dLocal)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
266
|
+
if (!dLocal)
|
|
267
|
+
return;
|
|
268
|
+
if (enableTime) {
|
|
269
|
+
// Emit UTC instant millis
|
|
270
|
+
onChange(dLocal.getTime());
|
|
210
271
|
}
|
|
272
|
+
else {
|
|
273
|
+
// Emit date-only string (local calendar day)
|
|
274
|
+
onChange(ymdFromLocalDate(dLocal));
|
|
275
|
+
}
|
|
276
|
+
setMonthAnchor(dLocal);
|
|
277
|
+
setDirty(false);
|
|
211
278
|
return;
|
|
212
279
|
}
|
|
213
280
|
const r = parseLooseRange(text);
|
|
214
281
|
if (r) {
|
|
215
|
-
|
|
282
|
+
// Range is date-only, based on local calendar parts
|
|
283
|
+
const start = `${r.start.getFullYear()}-${pad2(r.start.getMonth() + 1)}-${pad2(r.start.getDate())}`;
|
|
284
|
+
const end = `${r.end.getFullYear()}-${pad2(r.end.getMonth() + 1)}-${pad2(r.end.getDate())}`;
|
|
285
|
+
onChange({ start, end });
|
|
216
286
|
setMonthAnchor(r.start);
|
|
217
287
|
setDirty(false);
|
|
218
288
|
}
|
|
219
|
-
}, [text, mode, onChange]);
|
|
289
|
+
}, [text, mode, onChange, enableTime]);
|
|
220
290
|
const clear = useCallback(() => {
|
|
221
291
|
if (mode === 'single')
|
|
222
292
|
onChange(null);
|
|
@@ -241,7 +311,7 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
|
241
311
|
return (_jsx("div", { onClick: toggle, className: styles.triggerWrap, children: _jsx(Input, { ...inputProps, placeholder: (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder) !== null && _a !== void 0 ? _a : fallbackPlaceholder, value: dirty ? text : formatted, onInput: e => {
|
|
242
312
|
setDirty(true);
|
|
243
313
|
const raw = e.target.value;
|
|
244
|
-
const masked = mode === 'single' ? maskSingle(raw, enableTime) : maskRange(raw,
|
|
314
|
+
const masked = mode === 'single' ? maskSingle(raw, enableTime) : maskRange(raw, false);
|
|
245
315
|
setText(masked);
|
|
246
316
|
}, onBlur: commitTypedValue, onKeyDown: e => {
|
|
247
317
|
var _a;
|
|
@@ -256,9 +326,12 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
|
|
|
256
326
|
}, viewportPadding: 8, children: _jsxs("div", { className: cx(styles.panel, !!(presets === null || presets === void 0 ? void 0 : presets.length) && styles.panelWithPresets), children: [(presets === null || presets === void 0 ? void 0 : presets.length) ? (_jsxs("div", { className: styles.presetsCol, children: [_jsx("div", { className: styles.presetsLabel, children: "Forvalg" }), _jsxs("div", { className: styles.presetsList, children: [presets.map(p => (_jsx(Button, { variant: "outlined", size: "sm", onClick: () => {
|
|
257
327
|
var _a;
|
|
258
328
|
const r = p.getRange();
|
|
259
|
-
|
|
329
|
+
// Presets -> date-only range
|
|
330
|
+
const start = `${r.start.getFullYear()}-${pad2(r.start.getMonth() + 1)}-${pad2(r.start.getDate())}`;
|
|
331
|
+
const end = `${r.end.getFullYear()}-${pad2(r.end.getMonth() + 1)}-${pad2(r.end.getDate())}`;
|
|
332
|
+
onChange({ start, end });
|
|
260
333
|
setDirty(false);
|
|
261
|
-
setText(
|
|
334
|
+
setText(`${toMaskedFromYMD(start)} – ${toMaskedFromYMD(end)}`);
|
|
262
335
|
setMonthAnchor(r.start);
|
|
263
336
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
264
337
|
}, children: p.label }, p.label))), mode === 'range' && (_jsx(Button, { variant: "danger", size: "sm", onClick: clear, icon: _jsx(X, { size: 14 }), children: "Ryd" }))] })] })) : null, _jsxs("div", { className: styles.calendarArea, children: [_jsxs("div", { className: styles.header, children: [_jsx(Button, { variant: "outlined", size: "sm", "aria-label": "Forrige m\u00E5ned", icon: _jsx(ChevronLeft, { size: 16 }), onClick: () => setMonthAnchor(addMonthsLocal(monthAnchor, -1)) }), _jsx("div", { "aria-live": "polite", className: styles.headerTitle, children: monthFmt.format(monthAnchor) }), _jsx(Button, { variant: "outlined", size: "sm", "aria-label": "N\u00E6ste m\u00E5ned", icon: _jsx(ChevronRight, { size: 16 }), onClick: () => setMonthAnchor(addMonthsLocal(monthAnchor, 1)) })] }), _jsx("div", { className: styles.weekRow, "aria-hidden": true, children: Array.from({ length: 7 }, (_, i) => (i + weekStartsOn) % 7).map(dow => (_jsx("div", { className: styles.weekCell, children: weekdayFmt.format(dUTC(2024, 8, dow + 1)).slice(0, 2) }, dow))) }), _jsx("div", { ref: gridRef, role: "grid", "aria-label": "Kalender", tabIndex: 0, className: styles.grid, onMouseLeave: () => setHoverUTC(null), children: cellsUTC.map((utcDay, idx) => {
|
|
@@ -4,8 +4,18 @@ export declare function maskTimeHM(text: string): string;
|
|
|
4
4
|
export declare function maskSingle(text: string, enableTime: boolean): string;
|
|
5
5
|
export declare function maskRange(text: string, enableTime: boolean): string;
|
|
6
6
|
export declare const pad2: (n: number) => string;
|
|
7
|
+
export type DateOnly = string;
|
|
8
|
+
export declare function parseYMD(ymd: string): {
|
|
9
|
+
y: number;
|
|
10
|
+
m: number;
|
|
11
|
+
d: number;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function ymdFromLocalDate(dLocal: Date): DateOnly;
|
|
14
|
+
export declare function ymdFromUTCDateOnly(utcDay: Date): DateOnly;
|
|
15
|
+
export declare function utcMillisFromYMD(ymd: DateOnly): number;
|
|
16
|
+
export declare function localDateFromYMD(ymd: DateOnly): Date | null;
|
|
7
17
|
export declare function toMaskedFromDate(d: Date, enableTime: boolean): string;
|
|
8
|
-
export declare function
|
|
18
|
+
export declare function toMaskedFromYMD(ymd: DateOnly): string;
|
|
9
19
|
export declare function parseLooseDateOrDateTime(input: string): Date | null;
|
|
10
20
|
export declare function parseLooseRange(input: string): {
|
|
11
21
|
start: Date;
|
|
@@ -48,6 +48,45 @@ export function maskRange(text, enableTime) {
|
|
|
48
48
|
}
|
|
49
49
|
// Pad helper
|
|
50
50
|
export const pad2 = (n) => String(n).padStart(2, '0');
|
|
51
|
+
export function parseYMD(ymd) {
|
|
52
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
|
|
53
|
+
if (!m)
|
|
54
|
+
return null;
|
|
55
|
+
const y = +m[1];
|
|
56
|
+
const mo = +m[2];
|
|
57
|
+
const d = +m[3];
|
|
58
|
+
if (mo < 1 || mo > 12 || d < 1 || d > 31)
|
|
59
|
+
return null;
|
|
60
|
+
return { y, m: mo, d };
|
|
61
|
+
}
|
|
62
|
+
export function ymdFromLocalDate(dLocal) {
|
|
63
|
+
const y = dLocal.getFullYear();
|
|
64
|
+
const m = dLocal.getMonth() + 1;
|
|
65
|
+
const d = dLocal.getDate();
|
|
66
|
+
return `${y}-${pad2(m)}-${pad2(d)}`;
|
|
67
|
+
}
|
|
68
|
+
export function ymdFromUTCDateOnly(utcDay) {
|
|
69
|
+
const y = utcDay.getUTCFullYear();
|
|
70
|
+
const m = utcDay.getUTCMonth() + 1;
|
|
71
|
+
const d = utcDay.getUTCDate();
|
|
72
|
+
return `${y}-${pad2(m)}-${pad2(d)}`;
|
|
73
|
+
}
|
|
74
|
+
// Used only for ordering/comparing date-only values.
|
|
75
|
+
export function utcMillisFromYMD(ymd) {
|
|
76
|
+
const p = parseYMD(ymd);
|
|
77
|
+
if (!p)
|
|
78
|
+
return NaN;
|
|
79
|
+
return Date.UTC(p.y, p.m - 1, p.d);
|
|
80
|
+
}
|
|
81
|
+
// For anchoring the calendar grid safely from a date-only value.
|
|
82
|
+
export function localDateFromYMD(ymd) {
|
|
83
|
+
const p = parseYMD(ymd);
|
|
84
|
+
if (!p)
|
|
85
|
+
return null;
|
|
86
|
+
// local noon avoids DST edge cases for date-only anchors
|
|
87
|
+
return new Date(p.y, p.m - 1, p.d, 12, 0, 0, 0);
|
|
88
|
+
}
|
|
89
|
+
/* ---------- Formatting (UI shows local) ---------- */
|
|
51
90
|
// From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
|
|
52
91
|
export function toMaskedFromDate(d, enableTime) {
|
|
53
92
|
const dd = pad2(d.getDate());
|
|
@@ -58,15 +97,11 @@ export function toMaskedFromDate(d, enableTime) {
|
|
|
58
97
|
out += ` ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
59
98
|
return out;
|
|
60
99
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
return `${toMaskedFromDate(start, enableTime)} –`;
|
|
67
|
-
if (end)
|
|
68
|
-
return `– ${toMaskedFromDate(end, enableTime)}`;
|
|
69
|
-
return '';
|
|
100
|
+
export function toMaskedFromYMD(ymd) {
|
|
101
|
+
const p = parseYMD(ymd);
|
|
102
|
+
if (!p)
|
|
103
|
+
return '';
|
|
104
|
+
return `${pad2(p.d)}-${pad2(p.m)}-${p.y}`;
|
|
70
105
|
}
|
|
71
106
|
/* ---------- Parsing helpers (no deps) ---------- */
|
|
72
107
|
// Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
|
|
@@ -14,8 +14,9 @@ export type SelectProps<T> = Omit<InputContainerProps, 'children' | 'htmlFor' |
|
|
|
14
14
|
onClear?: () => void;
|
|
15
15
|
datakey?: string;
|
|
16
16
|
dataCy?: string;
|
|
17
|
+
disabled?: boolean;
|
|
17
18
|
tooltip?: React.ReactNode;
|
|
18
19
|
tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
|
|
19
20
|
};
|
|
20
|
-
export declare function Select<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, }: SelectProps<T>): React.ReactNode;
|
|
21
|
+
export declare function Select<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, disabled, }: SelectProps<T>): React.ReactNode;
|
|
21
22
|
export {};
|
|
@@ -10,11 +10,9 @@ import { Popover } from '../../popover/Popover';
|
|
|
10
10
|
import { InputContainer } from '../input-container/InputContainer';
|
|
11
11
|
export function Select({
|
|
12
12
|
// InputContainer props
|
|
13
|
-
label, error, helpText, orientation = 'vertical', labelWidth = '120px', fullWidth = true, required,
|
|
14
|
-
// ✅ tooltip props
|
|
15
|
-
tooltip, tooltipPlacement = 'right',
|
|
13
|
+
label, error, helpText, orientation = 'vertical', labelWidth = '120px', fullWidth = true, required, tooltip, tooltipPlacement = 'right',
|
|
16
14
|
// Select props
|
|
17
|
-
id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'outlined', onClear, datakey, dataCy, }) {
|
|
15
|
+
id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'outlined', onClear, datakey, dataCy, disabled, }) {
|
|
18
16
|
const generatedId = useId();
|
|
19
17
|
const controlId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
|
|
20
18
|
const describedById = `${controlId}-desc`;
|
|
@@ -74,10 +72,7 @@ id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'ou
|
|
|
74
72
|
ids.push(tooltipId);
|
|
75
73
|
return ids.length ? ids.join(' ') : undefined;
|
|
76
74
|
})();
|
|
77
|
-
return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button
|
|
78
|
-
// IMPORTANT: keep triggerProps last for events, but let our aria-describedby win
|
|
79
|
-
// We'll spread triggerProps and then override aria-describedby to include both.
|
|
80
|
-
, { ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
|
|
75
|
+
return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
|
|
81
76
|
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
|
82
77
|
onClick(e);
|
|
83
78
|
}, size: size, type: "button", "aria-haspopup": "listbox", "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : placeholder }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { ReactNode } from 'react';
|
|
2
|
-
import { ModalProps } from '../Modal';
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { type ModalProps } from '../Modal';
|
|
3
3
|
type ModalConfig = Omit<ModalProps, 'isOpen' | 'onRequestClose'> & {
|
|
4
4
|
onRequestClose?: () => void;
|
|
5
5
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext,
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
4
5
|
import { Modal } from '../Modal';
|
|
5
6
|
const ModalContext = createContext(undefined);
|
|
6
7
|
export function ModalProvider({ children }) {
|
|
7
8
|
const [isOpen, setIsOpen] = useState(false);
|
|
8
9
|
const [config, setConfig] = useState(null);
|
|
10
|
+
const [mounted, setMounted] = useState(false);
|
|
11
|
+
useEffect(() => setMounted(true), []);
|
|
9
12
|
// Holds the resolver for the current "confirm" call, if any
|
|
10
13
|
const pendingResolverRef = useRef(null);
|
|
11
14
|
const resolvePending = useCallback((value) => {
|
|
@@ -24,47 +27,43 @@ export function ModalProvider({ children }) {
|
|
|
24
27
|
setIsOpen(true);
|
|
25
28
|
}, [resolvePending]);
|
|
26
29
|
const handleRequestClose = useCallback(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
// Any non-explicit confirm click counts as "false"
|
|
30
|
+
var _a;
|
|
31
|
+
(_a = config === null || config === void 0 ? void 0 : config.onRequestClose) === null || _a === void 0 ? void 0 : _a.call(config);
|
|
31
32
|
resolvePending(false);
|
|
32
33
|
closeModal();
|
|
33
34
|
}, [config, closeModal, resolvePending]);
|
|
34
35
|
const confirm = useCallback((confirmConfig) => {
|
|
35
36
|
return new Promise(resolve => {
|
|
36
|
-
// cancel any previous pending confirm
|
|
37
37
|
resolvePending(false);
|
|
38
38
|
pendingResolverRef.current = resolve;
|
|
39
39
|
const { confirmLabel = 'Ok', cancelLabel = 'Annuller', ...rest } = confirmConfig;
|
|
40
|
-
const primaryAction = {
|
|
41
|
-
label: confirmLabel,
|
|
42
|
-
onClick: () => {
|
|
43
|
-
resolvePending(true);
|
|
44
|
-
closeModal();
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
const secondaryAction = {
|
|
48
|
-
label: cancelLabel,
|
|
49
|
-
onClick: () => {
|
|
50
|
-
resolvePending(false);
|
|
51
|
-
closeModal();
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
40
|
setConfig({
|
|
55
41
|
...rest,
|
|
56
|
-
primaryAction
|
|
57
|
-
|
|
42
|
+
primaryAction: {
|
|
43
|
+
label: confirmLabel,
|
|
44
|
+
onClick: () => {
|
|
45
|
+
resolvePending(true);
|
|
46
|
+
closeModal();
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
secondaryAction: {
|
|
50
|
+
label: cancelLabel,
|
|
51
|
+
onClick: () => {
|
|
52
|
+
resolvePending(false);
|
|
53
|
+
closeModal();
|
|
54
|
+
},
|
|
55
|
+
},
|
|
58
56
|
});
|
|
59
57
|
setIsOpen(true);
|
|
60
58
|
});
|
|
61
59
|
}, [closeModal, resolvePending]);
|
|
62
|
-
|
|
60
|
+
const modalNode = (_jsxs(ModalContext.Provider, { value: { openModal, closeModal, confirm }, children: [children, mounted &&
|
|
61
|
+
createPortal(_jsx(Modal, { ...(config !== null && config !== void 0 ? config : {}), isOpen: isOpen, onRequestClose: handleRequestClose, children: config === null || config === void 0 ? void 0 : config.children }), document.body)] }));
|
|
62
|
+
return modalNode;
|
|
63
63
|
}
|
|
64
64
|
export function useModal() {
|
|
65
65
|
const ctx = useContext(ModalContext);
|
|
66
|
-
if (!ctx)
|
|
66
|
+
if (!ctx)
|
|
67
67
|
throw new Error('useModal must be used within a ModalProvider');
|
|
68
|
-
}
|
|
69
68
|
return ctx;
|
|
70
69
|
}
|
|
@@ -1,37 +1,49 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
6
|
import styles from './Popover.module.css';
|
|
6
7
|
export const Popover = forwardRef(function Popover({ trigger: Trigger, children, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, }, ref) {
|
|
7
8
|
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, visible: false });
|
|
8
9
|
const containerRef = useRef(null);
|
|
9
10
|
const contentRef = useRef(null);
|
|
11
|
+
// avoid SSR/hydration mismatch
|
|
12
|
+
const [mounted, setMounted] = useState(false);
|
|
13
|
+
useEffect(() => setMounted(true), []);
|
|
10
14
|
const computeAndSetPosition = useCallback((show) => {
|
|
11
15
|
const container = containerRef.current;
|
|
12
16
|
const content = contentRef.current;
|
|
13
17
|
if (!container || !content)
|
|
14
18
|
return;
|
|
15
19
|
const triggerRect = container.getBoundingClientRect();
|
|
20
|
+
// Temporarily measure content size by forcing it into the layout.
|
|
16
21
|
const prevVis = content.style.visibility;
|
|
17
22
|
const prevDisp = content.style.display;
|
|
18
23
|
const prevMinWidth = content.style.minWidth;
|
|
19
24
|
const prevWidth = content.style.width;
|
|
25
|
+
const prevTop = content.style.top;
|
|
26
|
+
const prevLeft = content.style.left;
|
|
20
27
|
content.style.visibility = 'hidden';
|
|
21
28
|
content.style.display = 'block';
|
|
29
|
+
content.style.top = '0px';
|
|
30
|
+
content.style.left = '0px';
|
|
22
31
|
content.style.minWidth = minWidth;
|
|
23
32
|
content.style.width = 'auto';
|
|
24
33
|
const minWidthPx = content.offsetWidth;
|
|
25
34
|
const desiredWidthPx = Math.max(matchTriggerWidth ? triggerRect.width : 0, minWidthPx);
|
|
26
|
-
//
|
|
35
|
+
// Apply desired width and re-measure final size (height may depend on width).
|
|
27
36
|
content.style.minWidth = `${desiredWidthPx}px`;
|
|
28
37
|
content.style.width = `${desiredWidthPx}px`;
|
|
29
38
|
const contentWidth = content.offsetWidth;
|
|
30
39
|
const contentHeight = content.offsetHeight;
|
|
40
|
+
// Restore previous inline styles
|
|
31
41
|
content.style.visibility = prevVis;
|
|
32
42
|
content.style.display = prevDisp;
|
|
33
43
|
content.style.minWidth = prevMinWidth;
|
|
34
44
|
content.style.width = prevWidth;
|
|
45
|
+
content.style.top = prevTop;
|
|
46
|
+
content.style.left = prevLeft;
|
|
35
47
|
const vw = window.innerWidth;
|
|
36
48
|
const vh = window.innerHeight;
|
|
37
49
|
const spaceAbove = Math.max(0, triggerRect.top);
|
|
@@ -75,6 +87,12 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
|
|
|
75
87
|
open: () => computeAndSetPosition(true),
|
|
76
88
|
isOpen: () => !!pos.visible,
|
|
77
89
|
}), [closePopover, computeAndSetPosition, pos.visible]);
|
|
90
|
+
// Recompute position after open to account for content becoming visible / measured.
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
if (pos.visible)
|
|
93
|
+
computeAndSetPosition(true);
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
+
}, [pos.visible]);
|
|
78
96
|
useEffect(() => {
|
|
79
97
|
if (!pos.visible)
|
|
80
98
|
return;
|
|
@@ -102,17 +120,18 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
|
|
|
102
120
|
window.removeEventListener('scroll', handleReposition, true);
|
|
103
121
|
};
|
|
104
122
|
}, [pos.visible, closePopover, computeAndSetPosition]);
|
|
105
|
-
return (_jsxs("div", { className: styles.container, ref: containerRef,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
? children
|
|
116
|
-
|
|
123
|
+
return (_jsxs("div", { className: styles.container, ref: containerRef, children: [Trigger(openPopover, pos.visible ? _jsx(ChevronUp, { size: 20 }) : _jsx(ChevronDown, { size: 20 })), mounted &&
|
|
124
|
+
createPortal(_jsx("div", { ref: contentRef, className: styles.content, style: {
|
|
125
|
+
top: pos.top,
|
|
126
|
+
left: pos.left,
|
|
127
|
+
visibility: pos.visible ? 'visible' : 'hidden',
|
|
128
|
+
minWidth: pos.width ? `${pos.width}px` : minWidth,
|
|
129
|
+
width: pos.width ? `${pos.width}px` : undefined,
|
|
130
|
+
maxWidth: `calc(100vw - ${viewportPadding * 2}px)`,
|
|
131
|
+
maxHeight: `clamp(100px, calc(100vh - ${viewportPadding * 2}px), 400px)`,
|
|
132
|
+
overflow: 'auto',
|
|
133
|
+
}, role: "dialog", "aria-hidden": !pos.visible, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'popover-content', children: typeof children === 'function'
|
|
134
|
+
? children(closePopover)
|
|
135
|
+
: children }), document.body)] }));
|
|
117
136
|
});
|
|
118
137
|
Popover.displayName = 'Popover';
|
|
@@ -4,15 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
.content {
|
|
6
6
|
position: fixed;
|
|
7
|
-
top: 100%;
|
|
8
|
-
left: 0;
|
|
9
7
|
border: 1px solid var(--color-border-default);
|
|
10
8
|
background-color: var(--color-bg-surface);
|
|
11
9
|
border-radius: var(--border-radius-default);
|
|
12
10
|
padding: var(--spacing-sm) 0;
|
|
13
11
|
z-index: var(--z-popover);
|
|
14
|
-
max-width: 80vw;
|
|
15
|
-
max-height: 80vh;
|
|
16
12
|
overflow: auto;
|
|
17
13
|
box-shadow: var(--shadow-md);
|
|
18
14
|
}
|
|
@@ -82,7 +82,7 @@ export function TanstackTable(props) {
|
|
|
82
82
|
},
|
|
83
83
|
});
|
|
84
84
|
const columnItems = React.useMemo(() => mapDefsToColumnItems(columns), [columns]);
|
|
85
|
-
const visibleData =
|
|
85
|
+
const visibleData = table.getRowModel().rows.map(r => r.original);
|
|
86
86
|
const s = (_a = table.getState().sorting) === null || _a === void 0 ? void 0 : _a[0];
|
|
87
87
|
const sortById = (_b = s === null || s === void 0 ? void 0 : s.id) !== null && _b !== void 0 ? _b : undefined;
|
|
88
88
|
const sortDirection = s ? (s.desc ? 'desc' : 'asc') : null;
|