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.
@@ -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
- <Panel>
237
- <Calendar
238
- pickedDate={value}
239
- onDatePick={handlePickCalendarDate}
240
- minDate={minDate}
241
- maxDate={maxDate}
242
- events={events}
243
- showAcademicWeeks={showAcademicWeeks}
244
- academicWeeks={academicWeeks}
245
- />
246
- </Panel>
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={style}
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
- // Strategy,
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 (flip) fuiMiddleware.push(fuiFlip(typeof flip === 'object' ? flip : {}));
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
- // strategy: 'absolute',
137
+ strategy,
126
138
  whileElementsMounted: autoUpdate,
127
139
  });
128
140
 
@@ -76,7 +76,7 @@ const Toggle = forwardRef<Ref, ToggleProps>(
76
76
  <button
77
77
  data-test-id={NAME}
78
78
  ref={ref}
79
- role='toggle'
79
+ role='switch'
80
80
  aria-checked={checked}
81
81
  aria-disabled={disabled}
82
82
  className={style}
@@ -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
- <Panel>
283
- <Calendar
284
- pickedDate={value}
285
- onDatePick={handlePickCalendarDate}
286
- minDate={minDate}
287
- maxDate={maxDate}
288
- events={events}
289
- showAcademicWeeks={showAcademicWeeks}
290
- academicWeeks={academicWeeks}
291
- />
292
- </Panel>
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
  );
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.38.0",
5
+ "version": "0.38.1",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",