@canonical/react-components 3.5.0 → 3.6.0

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.
@@ -151,11 +151,18 @@ const ContextualMenu = _ref => {
151
151
  // becomes smaller.
152
152
  closePortal();
153
153
  } else {
154
- // Update the coordinates so that the menu stays relative to the
155
- // toggle button.
156
- updatePositionCoords();
154
+ // Only update if the coordinates have changed.
155
+ // The check fixes a bug with chrome, where an input receiving focus and
156
+ // opening the keyboard causes a resize and the keyboard closes right after
157
+ // opening.
158
+ const coords = parent.getBoundingClientRect();
159
+ if (JSON.stringify(coords) !== JSON.stringify(positionCoords)) {
160
+ // Update the coordinates so that the menu stays relative to the
161
+ // toggle button.
162
+ updatePositionCoords();
163
+ }
157
164
  }
158
- }, [closePortal, positionNode, updatePositionCoords]);
165
+ }, [closePortal, positionNode, positionCoords, updatePositionCoords]);
159
166
  const onScroll = (0, _react.useCallback)(e => {
160
167
  const parent = getPositionNode(wrapper.current, positionNode);
161
168
  // update position if the scroll event is triggered by the parent of the menu
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { HTMLProps, ReactNode } from "react";
2
+ import type { HTMLProps, ReactNode, RefObject } from "react";
3
3
  import { ClassName, PropsWithSpread } from "../../types";
4
4
  export type Props = PropsWithSpread<{
5
5
  /**
@@ -18,6 +18,10 @@ export type Props = PropsWithSpread<{
18
18
  * Function to handle closing the modal.
19
19
  */
20
20
  close?: () => void | null;
21
+ /**
22
+ * The element that will be focused upon opening the modal.
23
+ */
24
+ focusRef?: RefObject<HTMLElement | null>;
21
25
  /**
22
26
  * The title of the modal.
23
27
  */
@@ -36,5 +40,5 @@ export type Props = PropsWithSpread<{
36
40
  *
37
41
  * The modal component can be used to overlay an area of the screen which can contain a prompt, dialog or interaction.
38
42
  */
39
- export declare const Modal: ({ buttonRow, children, className, close, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
43
+ export declare const Modal: ({ buttonRow, children, className, close, focusRef, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
40
44
  export default Modal;
@@ -21,6 +21,7 @@ const Modal = _ref => {
21
21
  children,
22
22
  className,
23
23
  close,
24
+ focusRef,
24
25
  title,
25
26
  shouldPropagateClickEvent = false,
26
27
  closeOnOutsideClick = true,
@@ -33,6 +34,7 @@ const Modal = _ref => {
33
34
  const titleId = (0, _react.useId)();
34
35
  const shouldClose = (0, _react.useRef)(false);
35
36
  const modalRef = (0, _react.useRef)(null);
37
+ const closeButtonRef = (0, _react.useRef)(null);
36
38
  const focusableModalElements = (0, _react.useRef)(null);
37
39
  const handleTabKey = event => {
38
40
  if (focusableModalElements.current.length > 0) {
@@ -60,24 +62,18 @@ const Modal = _ref => {
60
62
  close();
61
63
  }
62
64
  };
63
- const keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
64
65
  (0, _react.useEffect)(() => {
65
- modalRef.current.focus();
66
- }, [modalRef]);
67
- const hasCloseButton = !!close;
68
- (0, _react.useEffect)(() => {
69
- var _focusableModalElemen;
70
- focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
71
- let focusIndex = 0;
72
- // when the close button is rendered, focus on the 2nd content element and not the close btn.
73
- if (hasCloseButton && focusableModalElements.current.length > 1) {
74
- focusIndex = 1;
66
+ if (focusRef !== null && focusRef !== void 0 && focusRef.current) {
67
+ focusRef.current.focus();
68
+ } else if (closeButtonRef.current) {
69
+ closeButtonRef.current.focus();
70
+ } else {
71
+ modalRef.current.focus();
75
72
  }
76
- (_focusableModalElemen = focusableModalElements.current[focusIndex]) === null || _focusableModalElemen === void 0 || _focusableModalElemen.focus({
77
- preventScroll: true
78
- });
79
- }, [hasCloseButton]);
73
+ focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
74
+ }, [focusRef]);
80
75
  (0, _react.useEffect)(() => {
76
+ const keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
81
77
  const keyDown = event => {
82
78
  const listener = keyListenersMap.get(event.code);
83
79
  return listener && listener(event);
@@ -130,11 +126,12 @@ const Modal = _ref => {
130
126
  }, /*#__PURE__*/_react.default.createElement("h2", {
131
127
  className: "p-modal__title",
132
128
  id: titleId
133
- }, title), hasCloseButton && /*#__PURE__*/_react.default.createElement("button", {
129
+ }, title), close && /*#__PURE__*/_react.default.createElement("button", {
134
130
  type: "button",
135
131
  className: "p-modal__close",
136
132
  "aria-label": "Close active modal",
137
- onClick: handleClose
133
+ onClick: handleClose,
134
+ ref: closeButtonRef
138
135
  }, "Close")), /*#__PURE__*/_react.default.createElement("div", {
139
136
  id: descriptionId
140
137
  }, children), !!buttonRow && /*#__PURE__*/_react.default.createElement("footer", {
@@ -4,3 +4,4 @@ declare const meta: Meta<typeof Modal>;
4
4
  export default meta;
5
5
  type Story = StoryObj<typeof Modal>;
6
6
  export declare const Default: Story;
7
+ export declare const Focus: Story;
@@ -3,8 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.Default = void 0;
6
+ exports.default = exports.Focus = exports.Default = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
+ var _Button = _interopRequireDefault(require("../Button"));
8
9
  var _Modal = _interopRequireDefault(require("./Modal"));
9
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
11
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
@@ -61,4 +62,35 @@ const Default = exports.Default = {
61
62
  }, /*#__PURE__*/_react.default.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/_react.default.createElement("br", null), "You cannot undo this action.")) : null);
62
63
  },
63
64
  name: "Default"
65
+ };
66
+ const Focus = exports.Focus = {
67
+ render: _ref2 => {
68
+ let {
69
+ closeOnOutsideClick
70
+ } = _ref2;
71
+ /* eslint-disable react-hooks/rules-of-hooks */
72
+ const [modalOpen, setModalOpen] = (0, _react.useState)(true);
73
+ const buttonRef = (0, _react.useRef)(null);
74
+ /* eslint-enable react-hooks/rules-of-hooks */
75
+
76
+ const closeHandler = () => setModalOpen(false);
77
+ return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("button", {
78
+ onClick: () => setModalOpen(true)
79
+ }, "Open modal"), modalOpen ? /*#__PURE__*/_react.default.createElement(_Modal.default, {
80
+ close: closeHandler,
81
+ title: "Confirm delete",
82
+ closeOnOutsideClick: closeOnOutsideClick,
83
+ buttonRow: /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("button", {
84
+ className: "u-no-margin--bottom",
85
+ onClick: closeHandler
86
+ }, "Cancel"), /*#__PURE__*/_react.default.createElement("button", {
87
+ className: "p-button--negative u-no-margin--bottom"
88
+ }, "Delete")),
89
+ focusRef: buttonRef
90
+ }, /*#__PURE__*/_react.default.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/_react.default.createElement("br", null), "You cannot undo this action."), /*#__PURE__*/_react.default.createElement("p", null, /*#__PURE__*/_react.default.createElement(_Button.default, {
91
+ appearance: "link",
92
+ ref: buttonRef
93
+ }, "More information"))) : null);
94
+ },
95
+ name: "Focus"
64
96
  };
@@ -147,11 +147,18 @@ var ContextualMenu = _ref => {
147
147
  // becomes smaller.
148
148
  closePortal();
149
149
  } else {
150
- // Update the coordinates so that the menu stays relative to the
151
- // toggle button.
152
- updatePositionCoords();
150
+ // Only update if the coordinates have changed.
151
+ // The check fixes a bug with chrome, where an input receiving focus and
152
+ // opening the keyboard causes a resize and the keyboard closes right after
153
+ // opening.
154
+ var coords = parent.getBoundingClientRect();
155
+ if (JSON.stringify(coords) !== JSON.stringify(positionCoords)) {
156
+ // Update the coordinates so that the menu stays relative to the
157
+ // toggle button.
158
+ updatePositionCoords();
159
+ }
153
160
  }
154
- }, [closePortal, positionNode, updatePositionCoords]);
161
+ }, [closePortal, positionNode, positionCoords, updatePositionCoords]);
155
162
  var onScroll = useCallback(e => {
156
163
  var parent = getPositionNode(wrapper.current, positionNode);
157
164
  // update position if the scroll event is triggered by the parent of the menu
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { HTMLProps, ReactNode } from "react";
2
+ import type { HTMLProps, ReactNode, RefObject } from "react";
3
3
  import { ClassName, PropsWithSpread } from "../../types";
4
4
  export type Props = PropsWithSpread<{
5
5
  /**
@@ -18,6 +18,10 @@ export type Props = PropsWithSpread<{
18
18
  * Function to handle closing the modal.
19
19
  */
20
20
  close?: () => void | null;
21
+ /**
22
+ * The element that will be focused upon opening the modal.
23
+ */
24
+ focusRef?: RefObject<HTMLElement | null>;
21
25
  /**
22
26
  * The title of the modal.
23
27
  */
@@ -36,5 +40,5 @@ export type Props = PropsWithSpread<{
36
40
  *
37
41
  * The modal component can be used to overlay an area of the screen which can contain a prompt, dialog or interaction.
38
42
  */
39
- export declare const Modal: ({ buttonRow, children, className, close, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
43
+ export declare const Modal: ({ buttonRow, children, className, close, focusRef, title, shouldPropagateClickEvent, closeOnOutsideClick, ...wrapperProps }: Props) => React.JSX.Element;
40
44
  export default Modal;
@@ -1,4 +1,4 @@
1
- var _excluded = ["buttonRow", "children", "className", "close", "title", "shouldPropagateClickEvent", "closeOnOutsideClick"];
1
+ var _excluded = ["buttonRow", "children", "className", "close", "focusRef", "title", "shouldPropagateClickEvent", "closeOnOutsideClick"];
2
2
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
3
3
  function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
4
4
  function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
@@ -15,6 +15,7 @@ export var Modal = _ref => {
15
15
  children,
16
16
  className,
17
17
  close,
18
+ focusRef,
18
19
  title,
19
20
  shouldPropagateClickEvent = false,
20
21
  closeOnOutsideClick = true
@@ -27,6 +28,7 @@ export var Modal = _ref => {
27
28
  var titleId = useId();
28
29
  var shouldClose = useRef(false);
29
30
  var modalRef = useRef(null);
31
+ var closeButtonRef = useRef(null);
30
32
  var focusableModalElements = useRef(null);
31
33
  var handleTabKey = event => {
32
34
  if (focusableModalElements.current.length > 0) {
@@ -54,24 +56,18 @@ export var Modal = _ref => {
54
56
  close();
55
57
  }
56
58
  };
57
- var keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
58
59
  useEffect(() => {
59
- modalRef.current.focus();
60
- }, [modalRef]);
61
- var hasCloseButton = !!close;
62
- useEffect(() => {
63
- var _focusableModalElemen;
64
- focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
65
- var focusIndex = 0;
66
- // when the close button is rendered, focus on the 2nd content element and not the close btn.
67
- if (hasCloseButton && focusableModalElements.current.length > 1) {
68
- focusIndex = 1;
60
+ if (focusRef !== null && focusRef !== void 0 && focusRef.current) {
61
+ focusRef.current.focus();
62
+ } else if (closeButtonRef.current) {
63
+ closeButtonRef.current.focus();
64
+ } else {
65
+ modalRef.current.focus();
69
66
  }
70
- (_focusableModalElemen = focusableModalElements.current[focusIndex]) === null || _focusableModalElemen === void 0 || _focusableModalElemen.focus({
71
- preventScroll: true
72
- });
73
- }, [hasCloseButton]);
67
+ focusableModalElements.current = modalRef.current.querySelectorAll(focusableElementSelectors);
68
+ }, [focusRef]);
74
69
  useEffect(() => {
70
+ var keyListenersMap = new Map([["Escape", handleEscKey], ["Tab", handleTabKey]]);
75
71
  var keyDown = event => {
76
72
  var listener = keyListenersMap.get(event.code);
77
73
  return listener && listener(event);
@@ -124,11 +120,12 @@ export var Modal = _ref => {
124
120
  }, /*#__PURE__*/React.createElement("h2", {
125
121
  className: "p-modal__title",
126
122
  id: titleId
127
- }, title), hasCloseButton && /*#__PURE__*/React.createElement("button", {
123
+ }, title), close && /*#__PURE__*/React.createElement("button", {
128
124
  type: "button",
129
125
  className: "p-modal__close",
130
126
  "aria-label": "Close active modal",
131
- onClick: handleClose
127
+ onClick: handleClose,
128
+ ref: closeButtonRef
132
129
  }, "Close")), /*#__PURE__*/React.createElement("div", {
133
130
  id: descriptionId
134
131
  }, children), !!buttonRow && /*#__PURE__*/React.createElement("footer", {
@@ -4,3 +4,4 @@ declare const meta: Meta<typeof Modal>;
4
4
  export default meta;
5
5
  type Story = StoryObj<typeof Modal>;
6
6
  export declare const Default: Story;
7
+ export declare const Focus: Story;
@@ -1,5 +1,6 @@
1
- import { useState } from "react";
1
+ import { useRef, useState } from "react";
2
2
  import React from "react";
3
+ import Button from "../Button";
3
4
  import Modal from "./Modal";
4
5
  var Template = args => {
5
6
  return /*#__PURE__*/React.createElement("div", {
@@ -53,4 +54,35 @@ export var Default = {
53
54
  }, /*#__PURE__*/React.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/React.createElement("br", null), "You cannot undo this action.")) : null);
54
55
  },
55
56
  name: "Default"
57
+ };
58
+ export var Focus = {
59
+ render: _ref2 => {
60
+ var {
61
+ closeOnOutsideClick
62
+ } = _ref2;
63
+ /* eslint-disable react-hooks/rules-of-hooks */
64
+ var [modalOpen, setModalOpen] = useState(true);
65
+ var buttonRef = useRef(null);
66
+ /* eslint-enable react-hooks/rules-of-hooks */
67
+
68
+ var closeHandler = () => setModalOpen(false);
69
+ return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
70
+ onClick: () => setModalOpen(true)
71
+ }, "Open modal"), modalOpen ? /*#__PURE__*/React.createElement(Modal, {
72
+ close: closeHandler,
73
+ title: "Confirm delete",
74
+ closeOnOutsideClick: closeOnOutsideClick,
75
+ buttonRow: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
76
+ className: "u-no-margin--bottom",
77
+ onClick: closeHandler
78
+ }, "Cancel"), /*#__PURE__*/React.createElement("button", {
79
+ className: "p-button--negative u-no-margin--bottom"
80
+ }, "Delete")),
81
+ focusRef: buttonRef
82
+ }, /*#__PURE__*/React.createElement("p", null, "This will permanently delete the user \"Simon\".", /*#__PURE__*/React.createElement("br", null), "You cannot undo this action."), /*#__PURE__*/React.createElement("p", null, /*#__PURE__*/React.createElement(Button, {
83
+ appearance: "link",
84
+ ref: buttonRef
85
+ }, "More information"))) : null);
86
+ },
87
+ name: "Focus"
56
88
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonical/react-components",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "author": {