uikit-react-public 0.38.0 → 0.38.1
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/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -2
- package/dist/components/Overlay/Overlay.d.ts +2 -1
- package/dist/components/Overlay/Overlay.stories.d.ts +1 -0
- package/dist/index.js +2490 -2460
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +24 -0
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +32 -11
- package/lib/components/Datepicker/subcomponents/Panel.tsx +3 -21
- package/lib/components/Overlay/Overlay.tsx +17 -5
- package/lib/components/Toggle/Toggle.tsx +1 -1
- package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +27 -11
- package/package.json +1 -1
|
@@ -149,6 +149,30 @@ describe('Datepicker', () => {
|
|
|
149
149
|
|
|
150
150
|
// Calendar should now be visible
|
|
151
151
|
expect(screen.getByTestId('ucl-uikit-calendar')).toBeInTheDocument();
|
|
152
|
+
const panel = screen.getByTestId('ucl-uikit-datepicker__panel');
|
|
153
|
+
expect(panel.parentElement).toHaveClass('ucl-overlay');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('Panel escapes a containing dialog via fixed positioning', () => {
|
|
157
|
+
customRender(
|
|
158
|
+
<dialog open>
|
|
159
|
+
<Datepicker />
|
|
160
|
+
</dialog>
|
|
161
|
+
);
|
|
162
|
+
const input = screen.getByTestId('ucl-uikit-datepicker__input');
|
|
163
|
+
fireEvent.focus(input);
|
|
164
|
+
|
|
165
|
+
const panel = screen.getByTestId('ucl-uikit-datepicker__panel');
|
|
166
|
+
expect(panel.parentElement).toHaveStyle({ position: 'fixed' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('Panel uses absolute positioning outside of a dialog', () => {
|
|
170
|
+
customRender(<Datepicker />);
|
|
171
|
+
const input = screen.getByTestId('ucl-uikit-datepicker__input');
|
|
172
|
+
fireEvent.focus(input);
|
|
173
|
+
|
|
174
|
+
const panel = screen.getByTestId('ucl-uikit-datepicker__panel');
|
|
175
|
+
expect(panel.parentElement).toHaveStyle({ position: 'absolute' });
|
|
152
176
|
});
|
|
153
177
|
|
|
154
178
|
test('value after minDate is set to null automatically', () => {
|
|
@@ -2,8 +2,10 @@ import { useRef, useEffect, useState, useCallback } from 'react';
|
|
|
2
2
|
import { css, cx } from '@emotion/css';
|
|
3
3
|
import { VisibleField, Panel } from '.';
|
|
4
4
|
import { Calendar } from '../..';
|
|
5
|
+
import Overlay from '../../Overlay';
|
|
5
6
|
import { parseInputValue } from '../utils';
|
|
6
7
|
import announce from '../../../utils/announce';
|
|
8
|
+
import { useTheme } from '../../../theme';
|
|
7
9
|
import type { DatepickerValue } from '../Datepicker.types';
|
|
8
10
|
import type { CalendarEvent, AcademicWeek } from '../../Calendar';
|
|
9
11
|
import type { InputProps } from './DatepickerInput';
|
|
@@ -57,7 +59,9 @@ const CustomDatepicker = ({
|
|
|
57
59
|
const effectiveInputRef = inputRef || internalInputRef;
|
|
58
60
|
const restoreFocusAfterClearRef = useRef(false);
|
|
59
61
|
|
|
62
|
+
const [theme] = useTheme();
|
|
60
63
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
64
|
+
const overlayViewportPadding = parseFloat(theme.margin.m32);
|
|
61
65
|
|
|
62
66
|
// Derived props (tidier than using `date?.getDate()`, etc, everywhere.)
|
|
63
67
|
const day = value?.getDate().toString().padStart(2, '0') ?? '';
|
|
@@ -233,17 +237,34 @@ const CustomDatepicker = ({
|
|
|
233
237
|
inputProps={inputProps}
|
|
234
238
|
/>
|
|
235
239
|
{panelOpen && (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
240
|
+
<Overlay
|
|
241
|
+
reference={effectiveRef}
|
|
242
|
+
placement='bottom-start'
|
|
243
|
+
// Escape the dialog's bounds via `fixed` positioning when it's too
|
|
244
|
+
// small to fit the panel, rather than squeezing/clipping it to fit.
|
|
245
|
+
strategy={
|
|
246
|
+
effectiveRef.current?.closest('dialog') ? 'fixed' : 'absolute'
|
|
247
|
+
}
|
|
248
|
+
flip={{ padding: overlayViewportPadding }}
|
|
249
|
+
shift={{
|
|
250
|
+
padding: overlayViewportPadding,
|
|
251
|
+
mainAxis: false,
|
|
252
|
+
crossAxis: true,
|
|
253
|
+
}}
|
|
254
|
+
offset={8}
|
|
255
|
+
>
|
|
256
|
+
<Panel>
|
|
257
|
+
<Calendar
|
|
258
|
+
pickedDate={value}
|
|
259
|
+
onDatePick={handlePickCalendarDate}
|
|
260
|
+
minDate={minDate}
|
|
261
|
+
maxDate={maxDate}
|
|
262
|
+
events={events}
|
|
263
|
+
showAcademicWeeks={showAcademicWeeks}
|
|
264
|
+
academicWeeks={academicWeeks}
|
|
265
|
+
/>
|
|
266
|
+
</Panel>
|
|
267
|
+
</Overlay>
|
|
247
268
|
)}
|
|
248
269
|
</div>
|
|
249
270
|
);
|
|
@@ -1,36 +1,18 @@
|
|
|
1
|
-
import { css, cx } from '@emotion/css';
|
|
2
|
-
|
|
3
1
|
interface PanelProps {
|
|
4
|
-
zIndex?: number;
|
|
5
2
|
children: React.ReactNode;
|
|
6
3
|
}
|
|
7
4
|
|
|
8
5
|
const NAME = 'ucl-uikit-datepicker__panel';
|
|
9
6
|
|
|
10
|
-
const Panel = ({
|
|
11
|
-
zIndex = 10,
|
|
12
|
-
children,
|
|
13
|
-
}: PanelProps) => {
|
|
14
|
-
|
|
15
|
-
const datepickerHeight = 48;
|
|
16
|
-
const gapFromDatepicker = 8;
|
|
17
|
-
|
|
18
|
-
const baseStyle = css`
|
|
19
|
-
position: absolute;
|
|
20
|
-
top: ${datepickerHeight + gapFromDatepicker}px;
|
|
21
|
-
z-index: ${zIndex};
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
const style = cx(NAME, baseStyle);
|
|
25
|
-
|
|
7
|
+
const Panel = ({ children }: PanelProps) => {
|
|
26
8
|
return (
|
|
27
9
|
<div
|
|
28
10
|
data-testid={NAME}
|
|
29
|
-
className={
|
|
11
|
+
className={NAME}
|
|
30
12
|
>
|
|
31
13
|
{children}
|
|
32
14
|
</div>
|
|
33
15
|
);
|
|
34
|
-
}
|
|
16
|
+
};
|
|
35
17
|
|
|
36
18
|
export default Panel;
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
Placement,
|
|
22
22
|
type FlipOptions,
|
|
23
23
|
type ShiftOptions,
|
|
24
|
-
|
|
24
|
+
type Strategy,
|
|
25
25
|
} from '@floating-ui/react-dom';
|
|
26
26
|
|
|
27
27
|
import { css, cx } from '@emotion/css';
|
|
@@ -44,6 +44,7 @@ export type OverlaySize = {
|
|
|
44
44
|
export interface OverlayProps extends HTMLAttributes<HTMLDivElement> {
|
|
45
45
|
reference: RefObject<HTMLElement | null>;
|
|
46
46
|
placement?: Placement;
|
|
47
|
+
strategy?: Strategy;
|
|
47
48
|
blanket?: boolean;
|
|
48
49
|
flip?: boolean | FlipOptions;
|
|
49
50
|
shift?: boolean | ShiftOptions;
|
|
@@ -62,6 +63,7 @@ const Overlay = forwardRef<Ref, OverlayProps>(
|
|
|
62
63
|
{
|
|
63
64
|
reference,
|
|
64
65
|
placement = 'bottom',
|
|
66
|
+
strategy = 'absolute',
|
|
65
67
|
blanket = false,
|
|
66
68
|
flip = true,
|
|
67
69
|
shift = true,
|
|
@@ -84,10 +86,21 @@ const Overlay = forwardRef<Ref, OverlayProps>(
|
|
|
84
86
|
useImperativeHandle<Ref, Ref>(forwardedRef, () => ref.current);
|
|
85
87
|
|
|
86
88
|
const fuiMiddleware = [];
|
|
87
|
-
if (
|
|
89
|
+
if (offset) fuiMiddleware.push(fuiOffset(offset));
|
|
90
|
+
if (flip)
|
|
91
|
+
fuiMiddleware.push(
|
|
92
|
+
fuiFlip({
|
|
93
|
+
// When escaping a containing element (e.g. a small Dialog) via
|
|
94
|
+
// `fixed` positioning, fall back to the originally-requested side
|
|
95
|
+
// rather than floating-ui's default "best fit" placement - bestFit
|
|
96
|
+
// can pick a placement that overlaps the reference entirely when
|
|
97
|
+
// nothing fits within the (still dialog-sized) boundary.
|
|
98
|
+
...(strategy === 'fixed' && { fallbackStrategy: 'initialPlacement' }),
|
|
99
|
+
...(typeof flip === 'object' ? flip : {}),
|
|
100
|
+
})
|
|
101
|
+
);
|
|
88
102
|
if (shift)
|
|
89
103
|
fuiMiddleware.push(fuiShift(typeof shift === 'object' ? shift : {}));
|
|
90
|
-
if (offset) fuiMiddleware.push(fuiOffset(offset));
|
|
91
104
|
if (size) {
|
|
92
105
|
const sizeOptions = typeof size === 'object' ? size : {};
|
|
93
106
|
const { padding = 0, matchReferenceWidth = false } = sizeOptions;
|
|
@@ -116,13 +129,12 @@ const Overlay = forwardRef<Ref, OverlayProps>(
|
|
|
116
129
|
x,
|
|
117
130
|
y,
|
|
118
131
|
refs,
|
|
119
|
-
strategy,
|
|
120
132
|
placement: currentPlacement,
|
|
121
133
|
} = useFloating({
|
|
122
134
|
open: true,
|
|
123
135
|
middleware: fuiMiddleware,
|
|
124
136
|
placement,
|
|
125
|
-
|
|
137
|
+
strategy,
|
|
126
138
|
whileElementsMounted: autoUpdate,
|
|
127
139
|
});
|
|
128
140
|
|
|
@@ -3,7 +3,9 @@ import { css, cx } from '@emotion/css';
|
|
|
3
3
|
import { VisibleField } from './';
|
|
4
4
|
import { Panel } from '../../Datepicker/subcomponents';
|
|
5
5
|
import { Calendar, Icon, IconButton } from '../..';
|
|
6
|
+
import Overlay from '../../Overlay';
|
|
6
7
|
import { parseInputValue } from '../../Datepicker/utils';
|
|
8
|
+
import { useTheme } from '../../../theme';
|
|
7
9
|
import type { DatepickerValue } from '../WeekPicker.types';
|
|
8
10
|
import type { CalendarEvent, AcademicWeek } from '../../Calendar';
|
|
9
11
|
|
|
@@ -81,7 +83,9 @@ const CustomDatepicker = ({
|
|
|
81
83
|
const internalInputRef = useRef<HTMLInputElement>(null);
|
|
82
84
|
const effectiveInputRef = inputRef || internalInputRef;
|
|
83
85
|
|
|
86
|
+
const [theme] = useTheme();
|
|
84
87
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
88
|
+
const overlayViewportPadding = parseFloat(theme.margin.m32);
|
|
85
89
|
|
|
86
90
|
// Derived props (tidier than using `date?.getDate()`, etc, everywhere.)
|
|
87
91
|
const day = value?.getDate().toString().padStart(2, '0') ?? '';
|
|
@@ -279,17 +283,29 @@ const CustomDatepicker = ({
|
|
|
279
283
|
<Icon.ChevronRight />
|
|
280
284
|
</IconButton>
|
|
281
285
|
{panelOpen && (
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
<Overlay
|
|
287
|
+
reference={effectiveRef}
|
|
288
|
+
placement='bottom-start'
|
|
289
|
+
flip={{ padding: overlayViewportPadding }}
|
|
290
|
+
shift={{
|
|
291
|
+
padding: overlayViewportPadding,
|
|
292
|
+
mainAxis: false,
|
|
293
|
+
crossAxis: true,
|
|
294
|
+
}}
|
|
295
|
+
offset={8}
|
|
296
|
+
>
|
|
297
|
+
<Panel>
|
|
298
|
+
<Calendar
|
|
299
|
+
pickedDate={value}
|
|
300
|
+
onDatePick={handlePickCalendarDate}
|
|
301
|
+
minDate={minDate}
|
|
302
|
+
maxDate={maxDate}
|
|
303
|
+
events={events}
|
|
304
|
+
showAcademicWeeks={showAcademicWeeks}
|
|
305
|
+
academicWeeks={academicWeeks}
|
|
306
|
+
/>
|
|
307
|
+
</Panel>
|
|
308
|
+
</Overlay>
|
|
293
309
|
)}
|
|
294
310
|
</div>
|
|
295
311
|
);
|