@lumx/react 3.9.2-alpha.9 → 3.9.3

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/package.json CHANGED
@@ -6,8 +6,8 @@
6
6
  "url": "https://github.com/lumapps/design-system/issues"
7
7
  },
8
8
  "dependencies": {
9
- "@lumx/core": "^3.9.2-alpha.9",
10
- "@lumx/icons": "^3.9.2-alpha.9",
9
+ "@lumx/core": "^3.9.3",
10
+ "@lumx/icons": "^3.9.3",
11
11
  "@popperjs/core": "^2.5.4",
12
12
  "body-scroll-lock": "^3.1.5",
13
13
  "classnames": "^2.3.2",
@@ -39,6 +39,7 @@
39
39
  "@testing-library/user-event": "^14.4.3",
40
40
  "@types/body-scroll-lock": "^2.6.1",
41
41
  "@types/classnames": "^2.2.9",
42
+ "@types/dom-view-transitions": "^1.0.5",
42
43
  "@types/jest": "^29.2.1",
43
44
  "@types/lodash": "^4.14.149",
44
45
  "@types/react": "^17.0.2",
@@ -110,5 +111,5 @@
110
111
  "build:storybook": "storybook build"
111
112
  },
112
113
  "sideEffects": false,
113
- "version": "3.9.2-alpha.9"
114
+ "version": "3.9.3"
114
115
  }
@@ -3,8 +3,9 @@ import React from 'react';
3
3
  import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
5
5
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
6
-
7
6
  import userEvent from '@testing-library/user-event';
7
+ import { VISUALLY_HIDDEN } from '@lumx/react/constants';
8
+
8
9
  import { DatePickerControlled, DatePickerControlledProps } from './DatePickerControlled';
9
10
  import { CLASSNAME } from './constants';
10
11
 
@@ -39,7 +40,7 @@ const queries = {
39
40
  screen.getByRole('spinbutton', {
40
41
  name: /année/i,
41
42
  }),
42
- getAccessibleMonthYear: (container: HTMLElement) => getByClassName(container, 'visually-hidden'),
43
+ getAccessibleMonthYear: (container: HTMLElement) => getByClassName(container, VISUALLY_HIDDEN),
43
44
  };
44
45
 
45
46
  describe(`<${DatePickerControlled.displayName}>`, () => {
@@ -1,6 +1,6 @@
1
1
  import React, { KeyboardEventHandler, forwardRef } from 'react';
2
2
  import classNames from 'classnames';
3
- import { DatePickerProps, Emphasis, FlexBox, IconButton, Text, TextField, Toolbar } from '@lumx/react';
3
+ import { DatePickerProps, Emphasis, FlexBox, IconButton, Text, TextField, TextFieldProps, Toolbar } from '@lumx/react';
4
4
  import { mdiChevronLeft, mdiChevronRight } from '@lumx/icons';
5
5
  import { Comp } from '@lumx/react/utils/type';
6
6
  import { getMonthCalendar } from '@lumx/react/utils/date/getMonthCalendar';
@@ -13,6 +13,7 @@ import { getYearDisplayName } from '@lumx/react/utils/date/getYearDisplayName';
13
13
  import { onEnterPressed } from '@lumx/react/utils/event';
14
14
  import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
15
15
  import { formatDayNumber } from '@lumx/react/utils/date/formatDayNumber';
16
+ import { VISUALLY_HIDDEN } from '@lumx/react/constants';
16
17
  import { CLASSNAME } from './constants';
17
18
 
18
19
  /**
@@ -61,36 +62,58 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
61
62
  return getMonthCalendar(localeObj, selectedMonth, minDate, maxDate);
62
63
  }, [locale, minDate, maxDate, selectedMonth]);
63
64
 
64
- const selectedYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric' }).slice(0, 4);
65
- const [textFieldYearValue, setTextFieldYearValue] = React.useState(selectedYear);
66
- const isYearValid = Number(textFieldYearValue) > 0 && Number(textFieldYearValue) <= 9999;
65
+ const selectedYear = selectedMonth.getFullYear();
66
+ const formattedYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric' }).slice(0, 4);
67
+ const [currentYear, setCurrentYear] = React.useState(String(selectedYear));
67
68
 
68
69
  // Updates month offset when validating year. Adds or removes 12 months per year when updating year value.
69
- const updateMonthOffset = React.useCallback(() => {
70
- if (isYearValid) {
71
- const yearNumber = selectedMonth.getFullYear();
72
- const offset = (Number(textFieldYearValue) - yearNumber) * 12;
73
- if (onMonthChange) {
74
- onMonthChange(addMonthResetDay(selectedMonth, offset));
70
+ const updateMonthOffset = React.useCallback(
71
+ (newYearValue: string) => {
72
+ const yearNumber = Number(newYearValue);
73
+ if (yearNumber < 0 && yearNumber >= 9999) {
74
+ return;
75
75
  }
76
- }
77
- }, [isYearValid, selectedMonth, textFieldYearValue, onMonthChange]);
78
76
 
79
- const monthYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
77
+ const previousYearNumber = selectedMonth.getFullYear();
78
+ const offset = (yearNumber - previousYearNumber) * 12;
79
+ onMonthChange?.(addMonthResetDay(selectedMonth, offset));
80
+ },
81
+ [selectedMonth, onMonthChange],
82
+ );
83
+
84
+ const onYearChange = React.useCallback<TextFieldProps['onChange']>(
85
+ (newYearValue, _, event) => {
86
+ setCurrentYear(newYearValue);
80
87
 
81
- // Year can only be validated by pressing Enter key or on Blur. The below handles the press Enter key case
82
- const handleKeyPress: KeyboardEventHandler = React.useMemo(
83
- () => onEnterPressed(updateMonthOffset),
88
+ // Detect if change is coming from the spin up/down arrows
89
+ const inputType = (event?.nativeEvent as any)?.inputType;
90
+ if (
91
+ // Chrome/Safari
92
+ !inputType ||
93
+ // Firefox
94
+ inputType === 'insertReplacementText'
95
+ ) {
96
+ updateMonthOffset(newYearValue);
97
+ }
98
+ },
84
99
  [updateMonthOffset],
85
100
  );
86
101
 
87
- // Required to update year in the TextField when the user changes year by using prev next month arrows
102
+ const updateYear = React.useCallback(() => {
103
+ updateMonthOffset(currentYear);
104
+ }, [updateMonthOffset, currentYear]);
105
+
106
+ const updateYearOnEnterPressed: KeyboardEventHandler = React.useMemo(
107
+ () => onEnterPressed(updateYear),
108
+ [updateYear],
109
+ );
110
+
111
+ const monthYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
112
+
113
+ // Update current year when selected year changes
88
114
  React.useEffect(() => {
89
- if (Number(textFieldYearValue) !== selectedMonth.getFullYear()) {
90
- setTextFieldYearValue(selectedMonth.getFullYear().toString());
91
- }
92
- // eslint-disable-next-line react-hooks/exhaustive-deps
93
- }, [selectedMonth]);
115
+ setCurrentYear(String(selectedYear));
116
+ }, [selectedYear]);
94
117
 
95
118
  const prevSelectedMonth = usePreviousValue(selectedMonth);
96
119
  const monthHasChanged = prevSelectedMonth && !isSameDay(selectedMonth, prevSelectedMonth);
@@ -101,7 +124,7 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
101
124
  if (monthHasChanged) setLabelAriaLive('polite');
102
125
  }, [monthHasChanged]);
103
126
 
104
- const label = getYearDisplayName(locale);
127
+ const yearLabel = getYearDisplayName(locale);
105
128
 
106
129
  return (
107
130
  <div ref={ref} className={`${CLASSNAME}`}>
@@ -125,7 +148,7 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
125
148
  }
126
149
  label={
127
150
  <>
128
- <span aria-live={labelAriaLive} className={onMonthChange ? 'visually-hidden' : ''} dir="auto">
151
+ <span aria-live={labelAriaLive} className={onMonthChange ? VISUALLY_HIDDEN : ''} dir="auto">
129
152
  {monthYear}
130
153
  </span>
131
154
  {onMonthChange && (
@@ -137,21 +160,21 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
137
160
  vAlign="center"
138
161
  dir="auto"
139
162
  >
140
- {RegExp(`(.*)(${selectedYear})(.*)`)
163
+ {RegExp(`(.*)(${formattedYear})(.*)`)
141
164
  .exec(monthYear)
142
165
  ?.slice(1)
143
166
  .filter((part) => part !== '')
144
167
  .map((part) =>
145
- part === selectedYear ? (
168
+ part === formattedYear ? (
146
169
  <TextField
147
- value={textFieldYearValue}
148
- aria-label={label}
149
- onChange={setTextFieldYearValue}
170
+ value={currentYear}
171
+ aria-label={yearLabel}
172
+ onChange={onYearChange}
150
173
  type="number"
151
174
  max={9999}
152
175
  min={0}
153
- onBlur={updateMonthOffset}
154
- onKeyPress={handleKeyPress}
176
+ onBlur={updateYear}
177
+ onKeyPress={updateYearOnEnterPressed}
155
178
  key="year"
156
179
  className={`${CLASSNAME}__year`}
157
180
  />
@@ -200,7 +223,7 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
200
223
  onClick={() => onChange(date)}
201
224
  >
202
225
  <span aria-hidden>{formatDayNumber(locale, date)}</span>
203
- <span className="visually-hidden">
226
+ <span className={VISUALLY_HIDDEN}>
204
227
  {date.toLocaleDateString(locale, {
205
228
  day: 'numeric',
206
229
  month: 'long',
@@ -55,6 +55,8 @@ const Inner: Comp<ImageLightboxProps, HTMLDivElement> = forwardRef((props, ref)
55
55
  closeButtonProps={closeButtonProps}
56
56
  focusElement={currentPaginationItemRef}
57
57
  {...forwardedProps}
58
+ // Disable the close on click away as we want a custom one here
59
+ preventAutoClose
58
60
  >
59
61
  <ClickAwayProvider childrenRefs={clickAwayChildrenRefs} callback={onClickAway}>
60
62
  <ImageSlideshow
@@ -109,7 +109,7 @@ export function useImageLightbox<P extends Partial<ImageLightboxProps>>(
109
109
  isOpen: true,
110
110
  onClose: () => {
111
111
  close();
112
- prevProps?.onClose?.();
112
+ propsRef.current?.onClose?.();
113
113
  },
114
114
  images,
115
115
  activeImageIndex: activeImageIndex || 0,
@@ -68,6 +68,8 @@ export interface TextFieldProps extends GenericProps, HasTheme {
68
68
  placeholder?: string;
69
69
  /** Reference to the wrapper. */
70
70
  textFieldRef?: Ref<HTMLDivElement>;
71
+ /** Native input type (only when `multiline` is disabled). */
72
+ type?: React.ComponentProps<'input'>['type'];
71
73
  /** Value. */
72
74
  value?: string;
73
75
  /** On blur callback. */
@@ -160,7 +162,7 @@ interface InputNativeProps {
160
162
  maxLength?: number;
161
163
  placeholder?: string;
162
164
  rows: number;
163
- type: string;
165
+ type: TextFieldProps['type'];
164
166
  name?: string;
165
167
  value?: string;
166
168
  setFocus(focus: boolean): void;
@@ -5,8 +5,9 @@ import { screen, render } from '@testing-library/react';
5
5
  import { queryAllByTagName, queryByClassName } from '@lumx/react/testing/utils/queries';
6
6
  import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
7
7
  import userEvent from '@testing-library/user-event';
8
-
9
8
  import { isFocusVisible } from '@lumx/react/utils/isFocusVisible';
9
+ import { VISUALLY_HIDDEN } from '@lumx/react/constants';
10
+
10
11
  import { Tooltip, TooltipProps } from './Tooltip';
11
12
 
12
13
  const CLASSNAME = Tooltip.className as string;
@@ -142,11 +143,11 @@ describe(`<${Tooltip.displayName}>`, () => {
142
143
  forceOpen: false,
143
144
  });
144
145
  expect(tooltip).toBeInTheDocument();
145
- expect(tooltip).toHaveClass('lumx-tooltip--is-hidden');
146
+ expect(tooltip).toHaveClass(VISUALLY_HIDDEN);
146
147
 
147
148
  const anchor = screen.getByRole('button', { name: 'Anchor' });
148
149
  await userEvent.hover(anchor);
149
- expect(tooltip).not.toHaveClass('lumx-tooltip--is-hidden');
150
+ expect(tooltip).not.toHaveClass(VISUALLY_HIDDEN);
150
151
  });
151
152
  });
152
153
 
@@ -4,7 +4,7 @@ import { createPortal } from 'react-dom';
4
4
 
5
5
  import classNames from 'classnames';
6
6
 
7
- import { DOCUMENT } from '@lumx/react/constants';
7
+ import { DOCUMENT, VISUALLY_HIDDEN } from '@lumx/react/constants';
8
8
  import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
9
9
  import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
10
10
  import { useMergeRefs } from '@lumx/react/utils/mergeRefs';
@@ -106,6 +106,7 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
106
106
  const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
107
107
  const isOpen = (isActivated || forceOpen) && !!label;
108
108
  const isMounted = !!label && (isOpen || closeMode === 'hide');
109
+ const isHidden = !isOpen && closeMode === 'hide';
109
110
  const wrappedChildren = useInjectTooltipRef({
110
111
  children,
111
112
  setAnchorElement,
@@ -139,8 +140,8 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
139
140
  handleBasicClasses({
140
141
  prefix: CLASSNAME,
141
142
  position,
142
- hidden: !isOpen && closeMode === 'hide',
143
143
  }),
144
+ isHidden && VISUALLY_HIDDEN,
144
145
  )}
145
146
  style={{ ...styles.popper, zIndex }}
146
147
  {...attributes.popper}
package/src/constants.ts CHANGED
@@ -20,3 +20,8 @@ export const DOCUMENT = typeof document !== 'undefined' ? document : undefined;
20
20
  * Check if we are running in a true browser
21
21
  */
22
22
  export const IS_BROWSER = typeof navigator !== 'undefined' && !navigator.userAgent.includes('jsdom');
23
+
24
+ /**
25
+ * Visually hidden a11y utility class name
26
+ */
27
+ export const VISUALLY_HIDDEN = 'visually-hidden';
@@ -5,9 +5,21 @@ import { MaybeElementOrRef } from '@lumx/react/utils/type';
5
5
  import { unref } from '../react/unref';
6
6
  import { getPrefersReducedMotion } from '../browser/getPrefersReducedMotion';
7
7
 
8
- function setTransitionViewName(elementRef: MaybeElementOrRef<HTMLElement>, name: string | null | undefined) {
9
- const element = unref(elementRef) as any;
10
- if (element) element.style.viewTransitionName = name;
8
+ function setupViewTransitionName(elementRef: MaybeElementOrRef<HTMLElement>, name: string) {
9
+ let originalName: string | null = null;
10
+ return {
11
+ set() {
12
+ const element = unref(elementRef);
13
+ if (!element) return;
14
+ originalName = element.style.viewTransitionName;
15
+ element.style.viewTransitionName = name;
16
+ },
17
+ unset() {
18
+ const element = unref(elementRef);
19
+ if (!element || originalName === null) return;
20
+ element.style.viewTransitionName = originalName;
21
+ },
22
+ };
11
23
  }
12
24
 
13
25
  /**
@@ -37,20 +49,20 @@ export async function startViewTransition({
37
49
  return;
38
50
  }
39
51
 
40
- // Set transition name on source element
41
- setTransitionViewName(viewTransitionName.source, viewTransitionName.name);
52
+ // Setup set/unset transition name on source & target
53
+ const sourceTransitionName = setupViewTransitionName(viewTransitionName.source, viewTransitionName.name);
54
+ const targetTransitionName = setupViewTransitionName(viewTransitionName.target, viewTransitionName.name);
55
+
56
+ sourceTransitionName.set();
42
57
 
43
58
  // Start view transition, apply changes & flush to DOM
44
59
  await start(() => {
45
- // Un-set transition name on source element
46
- setTransitionViewName(viewTransitionName.source, null);
60
+ sourceTransitionName.unset();
47
61
 
48
62
  flushSync(changes);
49
63
 
50
- // Set transition name on target element
51
- setTransitionViewName(viewTransitionName.target, viewTransitionName.name);
64
+ targetTransitionName.set();
52
65
  }).updateCallbackDone;
53
66
 
54
- // Un-set transition name on target element
55
- setTransitionViewName(viewTransitionName.target, null);
67
+ targetTransitionName.unset();
56
68
  }