@jobber/components 6.97.1 → 6.98.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/Menu-cjs.js CHANGED
@@ -43,31 +43,38 @@ function Menu({ activator, items, UNSAFE_className, UNSAFE_style, }) {
43
43
  // useRefocusOnActivator must come before useFocusTrap for them both to work
44
44
  jobberHooks.useRefocusOnActivator(visible);
45
45
  const menuRef = jobberHooks.useFocusTrap(visible);
46
+ const isLargeScreen = width >= SMALL_SCREEN_BREAKPOINT;
47
+ const middleware = React.useMemo(() => {
48
+ if (isLargeScreen) {
49
+ return [
50
+ floatingUi_react.offset(MENU_OFFSET),
51
+ floatingUi_react.flip({ fallbackPlacements: ["bottom-end", "top-start", "top-end"] }),
52
+ floatingUi_react.size({
53
+ apply({ availableHeight, elements }) {
54
+ // The inner element is the scrollable menu that requires the max height
55
+ const menuElement = elements.floating.querySelector('[role="menu"]');
56
+ if (menuElement) {
57
+ const viewportHeight = window.innerHeight;
58
+ const maxHeightVh = (viewportHeight * MENU_MAX_HEIGHT_PERCENTAGE) / 100;
59
+ const maxHeight$1 = maxHeight.calculateMaxHeight(availableHeight, {
60
+ maxHeight: maxHeightVh,
61
+ });
62
+ Object.assign(menuElement.style, {
63
+ maxHeight: `${maxHeight$1}px`,
64
+ });
65
+ }
66
+ },
67
+ }),
68
+ ];
69
+ }
70
+ return [];
71
+ }, [isLargeScreen]);
46
72
  const { refs, floatingStyles, context } = floatingUi_react.useFloating({
47
73
  open: visible,
48
74
  onOpenChange: setVisible,
49
75
  placement: "bottom-start",
50
76
  strategy: "fixed",
51
- middleware: [
52
- floatingUi_react.offset(MENU_OFFSET),
53
- floatingUi_react.flip({ fallbackPlacements: ["bottom-end", "top-start", "top-end"] }),
54
- floatingUi_react.size({
55
- apply({ availableHeight, elements }) {
56
- // The inner element is the scrollable menu that requires the max height
57
- const menuElement = elements.floating.querySelector('[role="menu"]');
58
- if (menuElement) {
59
- const viewportHeight = window.innerHeight;
60
- const maxHeightVh = (viewportHeight * MENU_MAX_HEIGHT_PERCENTAGE) / 100;
61
- const maxHeight$1 = maxHeight.calculateMaxHeight(availableHeight, {
62
- maxHeight: maxHeightVh,
63
- });
64
- Object.assign(menuElement.style, {
65
- maxHeight: `${maxHeight$1}px`,
66
- });
67
- }
68
- },
69
- }),
70
- ],
77
+ middleware,
71
78
  elements: {
72
79
  reference: referenceElement,
73
80
  },
@@ -75,7 +82,7 @@ function Menu({ activator, items, UNSAFE_className, UNSAFE_style, }) {
75
82
  });
76
83
  const dismiss = floatingUi_react.useDismiss(context);
77
84
  const { getFloatingProps } = floatingUi_react.useInteractions([dismiss]);
78
- const positionAttributes = width >= SMALL_SCREEN_BREAKPOINT
85
+ const positionAttributes = isLargeScreen
79
86
  ? {
80
87
  style: floatingStyles,
81
88
  }
package/dist/Menu-es.js CHANGED
@@ -1,8 +1,8 @@
1
- import React__default, { useState, useId, useRef } from 'react';
1
+ import React__default, { useState, useId, useMemo, useRef } from 'react';
2
2
  import classnames from 'classnames';
3
3
  import { AnimatePresence, motion } from 'framer-motion';
4
4
  import { useWindowDimensions, useRefocusOnActivator, useFocusTrap, useIsMounted } from '@jobber/hooks';
5
- import { u as useFloating, a as useDismiss, b as useInteractions, F as FloatingPortal, o as offset, f as flip, s as size, c as autoUpdate } from './floating-ui.react-es.js';
5
+ import { o as offset, f as flip, s as size, u as useFloating, a as useDismiss, b as useInteractions, F as FloatingPortal, c as autoUpdate } from './floating-ui.react-es.js';
6
6
  import { B as Button } from './Button-es.js';
7
7
  import { T as Typography } from './Typography-es.js';
8
8
  import { I as Icon } from './Icon-es.js';
@@ -41,31 +41,38 @@ function Menu({ activator, items, UNSAFE_className, UNSAFE_style, }) {
41
41
  // useRefocusOnActivator must come before useFocusTrap for them both to work
42
42
  useRefocusOnActivator(visible);
43
43
  const menuRef = useFocusTrap(visible);
44
+ const isLargeScreen = width >= SMALL_SCREEN_BREAKPOINT;
45
+ const middleware = useMemo(() => {
46
+ if (isLargeScreen) {
47
+ return [
48
+ offset(MENU_OFFSET),
49
+ flip({ fallbackPlacements: ["bottom-end", "top-start", "top-end"] }),
50
+ size({
51
+ apply({ availableHeight, elements }) {
52
+ // The inner element is the scrollable menu that requires the max height
53
+ const menuElement = elements.floating.querySelector('[role="menu"]');
54
+ if (menuElement) {
55
+ const viewportHeight = window.innerHeight;
56
+ const maxHeightVh = (viewportHeight * MENU_MAX_HEIGHT_PERCENTAGE) / 100;
57
+ const maxHeight = calculateMaxHeight(availableHeight, {
58
+ maxHeight: maxHeightVh,
59
+ });
60
+ Object.assign(menuElement.style, {
61
+ maxHeight: `${maxHeight}px`,
62
+ });
63
+ }
64
+ },
65
+ }),
66
+ ];
67
+ }
68
+ return [];
69
+ }, [isLargeScreen]);
44
70
  const { refs, floatingStyles, context } = useFloating({
45
71
  open: visible,
46
72
  onOpenChange: setVisible,
47
73
  placement: "bottom-start",
48
74
  strategy: "fixed",
49
- middleware: [
50
- offset(MENU_OFFSET),
51
- flip({ fallbackPlacements: ["bottom-end", "top-start", "top-end"] }),
52
- size({
53
- apply({ availableHeight, elements }) {
54
- // The inner element is the scrollable menu that requires the max height
55
- const menuElement = elements.floating.querySelector('[role="menu"]');
56
- if (menuElement) {
57
- const viewportHeight = window.innerHeight;
58
- const maxHeightVh = (viewportHeight * MENU_MAX_HEIGHT_PERCENTAGE) / 100;
59
- const maxHeight = calculateMaxHeight(availableHeight, {
60
- maxHeight: maxHeightVh,
61
- });
62
- Object.assign(menuElement.style, {
63
- maxHeight: `${maxHeight}px`,
64
- });
65
- }
66
- },
67
- }),
68
- ],
75
+ middleware,
69
76
  elements: {
70
77
  reference: referenceElement,
71
78
  },
@@ -73,7 +80,7 @@ function Menu({ activator, items, UNSAFE_className, UNSAFE_style, }) {
73
80
  });
74
81
  const dismiss = useDismiss(context);
75
82
  const { getFloatingProps } = useInteractions([dismiss]);
76
- const positionAttributes = width >= SMALL_SCREEN_BREAKPOINT
83
+ const positionAttributes = isLargeScreen
77
84
  ? {
78
85
  style: floatingStyles,
79
86
  }
@@ -1,3 +1,3 @@
1
1
  import React from "react";
2
2
  import type { ModalLegacyProps } from "./Modal.types";
3
- export declare function ModalLegacy({ open, title, size, dismissible, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, }: ModalLegacyProps): React.JSX.Element;
3
+ export declare function ModalLegacy({ open, title, size, dismissible, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, ariaLabel, }: ModalLegacyProps): React.JSX.Element;
@@ -1,30 +1,8 @@
1
1
  import type { MutableRefObject, PropsWithChildren, ReactNode } from "react";
2
- import type React from "react";
3
2
  import type { ExtendedRefs, FloatingContext, ReferenceType, UseInteractionsReturn } from "@floating-ui/react";
4
3
  import type { XOR } from "ts-xor";
5
4
  import type sizes from "./ModalSizes.module.css";
6
5
  import type { ButtonProps } from "../Button";
7
- export interface ModalProviderProps {
8
- readonly children: React.ReactNode;
9
- /**
10
- * Size of the modal.
11
- */
12
- readonly size?: keyof typeof sizes;
13
- /**
14
- * Whether the modal is open.
15
- */
16
- readonly open?: boolean;
17
- /**
18
- * Callback executed when the user wants to close/dismiss the Modal
19
- */
20
- readonly onRequestClose?: () => void;
21
- /**
22
- * Ref to specify the activator element. Useful if the activator can unmount
23
- * and focus needs to be returned to the activator element.
24
- */
25
- readonly activatorRef?: MutableRefObject<HTMLElement | null> | null;
26
- readonly dismissible?: boolean;
27
- }
28
6
  export type ModalContentProps = PropsWithChildren;
29
7
  export type ModalOverlayProps = PropsWithChildren;
30
8
  export interface ModalContextType {
@@ -66,6 +44,11 @@ export interface ModalContextType {
66
44
  * @default "ATL-Modal-Header"
67
45
  */
68
46
  readonly modalLabelledBy?: string;
47
+ /**
48
+ * Accessible name for the Modal.
49
+ * Intended for use when no Header/Title content is provided, as the Heading/title takes precedence over ariaLabel.
50
+ */
51
+ readonly ariaLabel?: string;
69
52
  /**
70
53
  * Floating-ui props to position the modal.
71
54
  */
@@ -119,5 +102,10 @@ export interface ModalLegacyProps {
119
102
  readonly tertiaryAction?: ButtonProps;
120
103
  onRequestClose?(): void;
121
104
  readonly version?: 1;
105
+ /**
106
+ * Accessible name for the Modal.
107
+ * Only required if no title is provided. Title takes precedence over ariaLabel.
108
+ */
109
+ readonly ariaLabel?: string;
122
110
  }
123
111
  export {};
@@ -11,6 +11,7 @@ export interface ModalProviderProps {
11
11
  readonly activatorRef?: MutableRefObject<HTMLElement | null> | null;
12
12
  readonly dismissible?: boolean;
13
13
  readonly modalLabelledBy?: string;
14
+ readonly ariaLabel?: string;
14
15
  }
15
- export declare function ModalProvider({ children, open, size, onRequestClose, activatorRef: refProp, dismissible, modalLabelledBy, }: ModalProviderProps): React.JSX.Element;
16
+ export declare function ModalProvider({ children, open, size, onRequestClose, activatorRef: refProp, dismissible, modalLabelledBy, ariaLabel, }: ModalProviderProps): React.JSX.Element;
16
17
  export declare function useModalContext(): ModalContextType;
@@ -0,0 +1 @@
1
+ export declare const MODAL_HEADER_ID: "ATL-Modal-Header";
@@ -32,12 +32,14 @@ var styles$1 = {"container":"y3M-9xoEnk0-","overlay":"zkyJp1mib-U-","modal":"gMP
32
32
 
33
33
  var sizes = {"small":"BSZvIAUzFEU-","large":"-ydIALYVvGg-","spinning":"_10FfgKITqY0-"};
34
34
 
35
- function ModalLegacy({ open = false, title, size, dismissible = true, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, }) {
35
+ const MODAL_HEADER_ID = "ATL-Modal-Header";
36
+
37
+ function ModalLegacy({ open = false, title, size, dismissible = true, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, ariaLabel, }) {
36
38
  const modalClassName = classnames(styles$1.modal, size && sizes[size]);
37
39
  jobberHooks.useRefocusOnActivator(open);
38
40
  const modalRef = jobberHooks.useFocusTrap(open);
39
41
  jobberHooks.useOnKeyDown(handleRequestClose, "Escape");
40
- const template = (React.createElement(framerMotion.AnimatePresence, null, open && (React.createElement("div", { ref: modalRef, role: "dialog", className: styles$1.container, tabIndex: 0 },
42
+ const template = (React.createElement(framerMotion.AnimatePresence, null, open && (React.createElement("div", { ref: modalRef, role: "dialog", className: styles$1.container, tabIndex: 0, "aria-modal": "true", "aria-labelledby": title ? MODAL_HEADER_ID : undefined, "aria-label": ariaLabel },
41
43
  React.createElement(framerMotion.motion.div, { key: styles$1.overlay, className: styles$1.overlay, onClick: onRequestClose, initial: { opacity: 0 }, animate: { opacity: 0.8 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }),
42
44
  React.createElement(framerMotion.motion.div, { key: styles$1.modal, className: modalClassName, initial: { scale: 0.9, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.9, opacity: 0 }, transition: {
43
45
  duration: 0.2,
@@ -57,7 +59,7 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
57
59
  }
58
60
  function Header({ title, dismissible, onRequestClose }) {
59
61
  return (React.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
60
- React.createElement(Heading.Heading, { level: 2 }, title),
62
+ React.createElement(Heading.Heading, { level: 2, id: MODAL_HEADER_ID }, title),
61
63
  dismissible && (React.createElement("div", { className: styles$1.closeButton },
62
64
  React.createElement(ButtonDismiss.ButtonDismiss, { onClick: onRequestClose, ariaLabel: "Close modal" })))));
63
65
  }
@@ -120,7 +122,7 @@ const ModalContext = React.createContext({
120
122
  dismissible: true,
121
123
  getFloatingProps: identity.identity,
122
124
  });
123
- function ModalProvider({ children, open = false, size, onRequestClose = noop.noop, activatorRef: refProp, dismissible = true, modalLabelledBy = "ATL-Modal-Header", }) {
125
+ function ModalProvider({ children, open = false, size, onRequestClose = noop.noop, activatorRef: refProp, dismissible = true, modalLabelledBy = MODAL_HEADER_ID, ariaLabel, }) {
124
126
  const { floatingRefs, floatingContext, nodeId, activatorRef, parentId, getFloatingProps, } = useModal({
125
127
  open,
126
128
  activatorRef: refProp,
@@ -136,6 +138,7 @@ function ModalProvider({ children, open = false, size, onRequestClose = noop.noo
136
138
  floatingNodeId: nodeId,
137
139
  dismissible,
138
140
  modalLabelledBy,
141
+ ariaLabel,
139
142
  getFloatingProps,
140
143
  } }, children));
141
144
  if (parentId) {
@@ -168,8 +171,8 @@ function ModalHeader({ title, children }) {
168
171
  if (children) {
169
172
  return React.createElement(React.Fragment, null, children);
170
173
  }
171
- return (React.createElement("div", { className: header, "data-testid": "ATL-Modal-Header", id: modalLabelledBy },
172
- React.createElement(Heading.Heading, { level: 2 }, title),
174
+ return (React.createElement("div", { className: header, "data-testid": MODAL_HEADER_ID },
175
+ React.createElement(Heading.Heading, { level: 2, id: modalLabelledBy }, title),
173
176
  dismissible && (React.createElement("div", { className: dismissButton },
174
177
  React.createElement(ButtonDismiss.ButtonDismiss, { onClick: onRequestClose, ariaLabel: "Close modal" })))));
175
178
  }
@@ -198,7 +201,7 @@ function ModalOverlay({ children }) {
198
201
  children));
199
202
  }
200
203
  function ModalContent({ children }) {
201
- const { open, floatingContext, activatorRef, floatingRefs, size, floatingNodeId, modalLabelledBy, getFloatingProps, } = useModalContext();
204
+ const { open, floatingContext, activatorRef, floatingRefs, size, floatingNodeId, modalLabelledBy, ariaLabel, getFloatingProps, } = useModalContext();
202
205
  const { modal } = useModalStyles(size);
203
206
  return (React.createElement(framerMotion.AnimatePresence, null, open && (React.createElement(floatingUi_react.FloatingNode, { id: floatingNodeId },
204
207
  React.createElement(floatingUi_react.FloatingPortal, null,
@@ -213,6 +216,8 @@ function ModalContent({ children }) {
213
216
  role: "dialog",
214
217
  className: modal,
215
218
  "aria-labelledby": modalLabelledBy,
219
+ "aria-label": ariaLabel,
220
+ "aria-modal": true,
216
221
  })), children))))))))));
217
222
  }
218
223
 
@@ -30,12 +30,14 @@ var styles$1 = {"container":"y3M-9xoEnk0-","overlay":"zkyJp1mib-U-","modal":"gMP
30
30
 
31
31
  var sizes = {"small":"BSZvIAUzFEU-","large":"-ydIALYVvGg-","spinning":"_10FfgKITqY0-"};
32
32
 
33
- function ModalLegacy({ open = false, title, size, dismissible = true, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, }) {
33
+ const MODAL_HEADER_ID = "ATL-Modal-Header";
34
+
35
+ function ModalLegacy({ open = false, title, size, dismissible = true, children, primaryAction, secondaryAction, tertiaryAction, onRequestClose, ariaLabel, }) {
34
36
  const modalClassName = classnames(styles$1.modal, size && sizes[size]);
35
37
  useRefocusOnActivator(open);
36
38
  const modalRef = useFocusTrap(open);
37
39
  useOnKeyDown(handleRequestClose, "Escape");
38
- const template = (React__default.createElement(AnimatePresence, null, open && (React__default.createElement("div", { ref: modalRef, role: "dialog", className: styles$1.container, tabIndex: 0 },
40
+ const template = (React__default.createElement(AnimatePresence, null, open && (React__default.createElement("div", { ref: modalRef, role: "dialog", className: styles$1.container, tabIndex: 0, "aria-modal": "true", "aria-labelledby": title ? MODAL_HEADER_ID : undefined, "aria-label": ariaLabel },
39
41
  React__default.createElement(motion.div, { key: styles$1.overlay, className: styles$1.overlay, onClick: onRequestClose, initial: { opacity: 0 }, animate: { opacity: 0.8 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }),
40
42
  React__default.createElement(motion.div, { key: styles$1.modal, className: modalClassName, initial: { scale: 0.9, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.9, opacity: 0 }, transition: {
41
43
  duration: 0.2,
@@ -55,7 +57,7 @@ function ModalLegacy({ open = false, title, size, dismissible = true, children,
55
57
  }
56
58
  function Header({ title, dismissible, onRequestClose }) {
57
59
  return (React__default.createElement("div", { className: styles$1.header, "data-testid": "modal-header" },
58
- React__default.createElement(Heading, { level: 2 }, title),
60
+ React__default.createElement(Heading, { level: 2, id: MODAL_HEADER_ID }, title),
59
61
  dismissible && (React__default.createElement("div", { className: styles$1.closeButton },
60
62
  React__default.createElement(ButtonDismiss, { onClick: onRequestClose, ariaLabel: "Close modal" })))));
61
63
  }
@@ -118,7 +120,7 @@ const ModalContext = createContext({
118
120
  dismissible: true,
119
121
  getFloatingProps: identity,
120
122
  });
121
- function ModalProvider({ children, open = false, size, onRequestClose = noop, activatorRef: refProp, dismissible = true, modalLabelledBy = "ATL-Modal-Header", }) {
123
+ function ModalProvider({ children, open = false, size, onRequestClose = noop, activatorRef: refProp, dismissible = true, modalLabelledBy = MODAL_HEADER_ID, ariaLabel, }) {
122
124
  const { floatingRefs, floatingContext, nodeId, activatorRef, parentId, getFloatingProps, } = useModal({
123
125
  open,
124
126
  activatorRef: refProp,
@@ -134,6 +136,7 @@ function ModalProvider({ children, open = false, size, onRequestClose = noop, ac
134
136
  floatingNodeId: nodeId,
135
137
  dismissible,
136
138
  modalLabelledBy,
139
+ ariaLabel,
137
140
  getFloatingProps,
138
141
  } }, children));
139
142
  if (parentId) {
@@ -166,8 +169,8 @@ function ModalHeader({ title, children }) {
166
169
  if (children) {
167
170
  return React__default.createElement(React__default.Fragment, null, children);
168
171
  }
169
- return (React__default.createElement("div", { className: header, "data-testid": "ATL-Modal-Header", id: modalLabelledBy },
170
- React__default.createElement(Heading, { level: 2 }, title),
172
+ return (React__default.createElement("div", { className: header, "data-testid": MODAL_HEADER_ID },
173
+ React__default.createElement(Heading, { level: 2, id: modalLabelledBy }, title),
171
174
  dismissible && (React__default.createElement("div", { className: dismissButton },
172
175
  React__default.createElement(ButtonDismiss, { onClick: onRequestClose, ariaLabel: "Close modal" })))));
173
176
  }
@@ -196,7 +199,7 @@ function ModalOverlay({ children }) {
196
199
  children));
197
200
  }
198
201
  function ModalContent({ children }) {
199
- const { open, floatingContext, activatorRef, floatingRefs, size, floatingNodeId, modalLabelledBy, getFloatingProps, } = useModalContext();
202
+ const { open, floatingContext, activatorRef, floatingRefs, size, floatingNodeId, modalLabelledBy, ariaLabel, getFloatingProps, } = useModalContext();
200
203
  const { modal } = useModalStyles(size);
201
204
  return (React__default.createElement(AnimatePresence, null, open && (React__default.createElement(FloatingNode, { id: floatingNodeId },
202
205
  React__default.createElement(FloatingPortal, null,
@@ -211,6 +214,8 @@ function ModalContent({ children }) {
211
214
  role: "dialog",
212
215
  className: modal,
213
216
  "aria-labelledby": modalLabelledBy,
217
+ "aria-label": ariaLabel,
218
+ "aria-modal": true,
214
219
  })), children))))))))));
215
220
  }
216
221
 
package/dist/styles.css CHANGED
@@ -2899,6 +2899,7 @@ a._7BLGtYNuJOU-.zgRx3ehZ2z8-:hover {
2899
2899
  right: 0;
2900
2900
  bottom: 0;
2901
2901
  left: 0;
2902
+ max-height: 70vh;
2902
2903
  }
2903
2904
  }
2904
2905
 
@@ -2909,6 +2910,7 @@ a._7BLGtYNuJOU-.zgRx3ehZ2z8-:hover {
2909
2910
  right: 0;
2910
2911
  bottom: 0;
2911
2912
  left: 0;
2913
+ max-height: 70vh;
2912
2914
  }
2913
2915
  }
2914
2916
 
@@ -0,0 +1,6 @@
1
+ export interface ViewportDimensions {
2
+ readonly width?: number;
3
+ readonly height?: number;
4
+ }
5
+ export declare function mockViewport({ width, height, }: ViewportDimensions): () => void;
6
+ export declare function withMockedViewport(dimensions: ViewportDimensions, run: () => Promise<void> | void): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "6.97.1",
3
+ "version": "6.98.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -541,5 +541,5 @@
541
541
  "> 1%",
542
542
  "IE 10"
543
543
  ],
544
- "gitHead": "7ad3ab047c32d137b34e2b9d8c067b16f9d46a49"
544
+ "gitHead": "242c797fed244210585dd6254cba023fe5612925"
545
545
  }