@truedat/core 8.14.1 → 8.14.2

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.14.1",
3
+ "version": "8.14.2",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -57,7 +57,7 @@
57
57
  "@testing-library/jest-dom": "^6.6.3",
58
58
  "@testing-library/react": "^16.3.0",
59
59
  "@testing-library/user-event": "^14.6.1",
60
- "@truedat/test": "8.14.1",
60
+ "@truedat/test": "8.14.2",
61
61
  "identity-obj-proxy": "^3.0.0",
62
62
  "jest": "^29.7.0",
63
63
  "redux-saga-test-plan": "^4.0.6"
@@ -99,5 +99,5 @@
99
99
  "swr": "^2.3.3",
100
100
  "turndown": "^7.2.2"
101
101
  },
102
- "gitHead": "391e4e51eebb33b37b9676e541cda5f6f262e5e7"
102
+ "gitHead": "3814d239579315d817ba0e3467771e6a128e5788"
103
103
  }
@@ -1,4 +1,4 @@
1
- import { useId } from "react";
1
+ import { useCallback, useId, useRef } from "react";
2
2
  import PropTypes from "prop-types";
3
3
  import { useIntl, FormattedMessage } from "react-intl";
4
4
  import { Button, Icon, Modal } from "semantic-ui-react";
@@ -26,9 +26,44 @@ export const TrueModal = ({
26
26
  onCancel,
27
27
  onCreate,
28
28
  className,
29
+ closeDisabled = false,
29
30
  ...modalProps
30
31
  }) => {
31
32
  const titleId = useId();
33
+ const headingRef = useRef(null);
34
+ const setHeading = useCallback((node) => {
35
+ headingRef.current = node;
36
+ if (!node) return undefined;
37
+ const frame = requestAnimationFrame(() => {
38
+ node.closest(".ui.modal")?.querySelector(".true-modal-close")?.focus();
39
+ });
40
+ return () => cancelAnimationFrame(frame);
41
+ }, []);
42
+ const handleKeyDown = (event) => {
43
+ modalProps.onKeyDown?.(event);
44
+ if (event.key !== "Tab" || event.defaultPrevented) return;
45
+ const dialog = headingRef.current?.closest(".ui.modal");
46
+ const controls = Array.from(
47
+ dialog?.querySelectorAll(
48
+ 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex="0"]',
49
+ ) || [],
50
+ ).filter(
51
+ (node) =>
52
+ node.getClientRects().length &&
53
+ !node.closest("[aria-disabled=true], fieldset[disabled]"),
54
+ );
55
+ const first = controls[0];
56
+ const last = controls[controls.length - 1];
57
+ if (
58
+ first &&
59
+ (!dialog.contains(document.activeElement) ||
60
+ (event.shiftKey && document.activeElement === first) ||
61
+ (!event.shiftKey && document.activeElement === last))
62
+ ) {
63
+ event.preventDefault();
64
+ (event.shiftKey ? last : first).focus();
65
+ }
66
+ };
32
67
  const { formatMessage } = useIntl();
33
68
  const defaultActions = _.compact([
34
69
  onCancel && {
@@ -50,6 +85,7 @@ export const TrueModal = ({
50
85
  <Modal
51
86
  {..._.omit(["header", "content", "closeIcon"])(modalProps)}
52
87
  className={_.compact(["true-modal", className]).join(" ")}
88
+ onKeyDown={handleKeyDown}
53
89
  role="dialog"
54
90
  aria-labelledby={
55
91
  modalProps["aria-labelledby"] ||
@@ -58,6 +94,7 @@ export const TrueModal = ({
58
94
  closeIcon={
59
95
  <button
60
96
  type="button"
97
+ disabled={closeDisabled}
61
98
  className="true-modal-close"
62
99
  style={{ color: headerTextColor }}
63
100
  aria-label={formatMessage({
@@ -71,7 +108,7 @@ export const TrueModal = ({
71
108
  >
72
109
  <Modal.Header style={{ background: headerColor, color: headerTextColor }}>
73
110
  {icon ? <Icon name={icon} size="large" /> : null}
74
- <div className="true-modal-heading">
111
+ <div ref={setHeading} className="true-modal-heading">
75
112
  <span id={titleId}>{title}</span>
76
113
  {subtitle != null ? (
77
114
  <p className="true-modal-subtitle">{subtitle}</p>
@@ -132,6 +169,7 @@ TrueModal.propTypes = {
132
169
  onCancel: PropTypes.func,
133
170
  onCreate: PropTypes.func,
134
171
  className: PropTypes.string,
172
+ closeDisabled: PropTypes.bool,
135
173
  };
136
174
 
137
175
  export default TrueModal;
@@ -1,4 +1,4 @@
1
- import { screen } from "@testing-library/react";
1
+ import { act, screen, waitFor } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { render } from "@truedat/test/render";
4
4
  import { TrueModal } from "../index";
@@ -190,4 +190,27 @@ describe("<TrueModal />", () => {
190
190
  rerender(<TrueModal open={false} title="Title" />);
191
191
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
192
192
  });
193
+ it("focuses the dialog and wraps keyboard focus within its controls", async () => {
194
+ const rects = jest
195
+ .spyOn(HTMLElement.prototype, "getClientRects")
196
+ .mockReturnValue([{}]);
197
+ renderModal({
198
+ children: <input aria-label="Name" />,
199
+ actions: [{ key: "save", content: "Save" }],
200
+ });
201
+ act(() => jest.advanceTimersByTime(20));
202
+ const close = screen.getByRole("button", { name: "Close" });
203
+ await waitFor(() => expect(close).toHaveFocus());
204
+ const user = setupUser();
205
+ await user.tab({ shift: true });
206
+ expect(screen.getByRole("button", { name: "Save" })).toHaveFocus();
207
+ await user.tab();
208
+ expect(close).toHaveFocus();
209
+ rects.mockRestore();
210
+ });
211
+
212
+ it("exposes disabled semantics on the close control while an operation is pending", () => {
213
+ renderModal({ closeDisabled: true });
214
+ expect(screen.getByRole("button", { name: "Close" })).toBeDisabled();
215
+ });
193
216
  });