Skip to content

Commit 618453c

Browse files
Merge branch 'develop' into feature/reactFlowEdgeNodeLevels-CMEM-7251
2 parents f89a901 + d07ed6f commit 618453c

17 files changed

Lines changed: 1091 additions & 87 deletions

‎CHANGELOG.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
3030
- already set `id` values and connections are never overwritten
3131
- ID references created by the field item are removed again if their part is removed from the field item
3232
- `preventAriaAttribution` property: prevents this automatic connection of the field item parts
33+
- `<Modal />`
34+
- `role`, `aria-label`, `aria-labelledby` and `aria-describedby` properties: they are set on the dialog element inside the modal overlay
35+
- `role` is `dialog` by default, but it is removed again if neither a label nor a description is available; a console warning points this out when the modal is opened
36+
- `aria-modal` is set together with the `role`, so it is left out as well if the `role` was removed
37+
- it is `true` for the modal that was opened last according to the `ModalContext`, otherwise it is `false`
38+
- if no `ModalContext` is provided, then the modals cannot know about each other, so each of them claims modality
39+
- `<SimpleDialog />`
40+
- `role` and the aria attributes are set automatically now if they are not given
41+
- `role` is `alertdialog` if an `intent` state is set that describes an alert (`success`, `warning`, `danger` or `info`), otherwise it is `dialog`
42+
- for those alert intent states the content area gets an `id` and is referred by the dialog via `aria-describedby`
43+
- explicitly given values are never overwritten
44+
- if neither `title`, `aria-label` nor `aria-labelledby` is given for an alert, then the `intent` level is used as fallback for `aria-label`, so the alert dialog always has an accessible name
3345
- new `utils` methods:
3446
- `truncateMarkdownDisplay`: helper function to iterate over `Markdown` renderings to improve the experienced `cutOff` value
3547
- new icons:
@@ -44,6 +56,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
4456
- the build of the ESM distribution needs a synchronous `import.meta.resolve`, which is only available since this version
4557
- `<FieldItem />`
4658
- the used `Label` element gets the `eccgui-fielditem__label` class now
59+
- `<AlertDialog />`
60+
- always uses `role="alertdialog"` now
61+
- the `role` property is not accepted anymore
62+
- `ModalContext`
63+
- a change of the stack of open modals re-renders the consumers of the context now, this way modals can react on modals that are opened on top of them, e.g. to hand over `aria-modal`
64+
- before only an internal reference was updated, which never triggered any re-render
65+
- the component that provides the context via `useModalContext` is re-rendered on every change of the stack, but not if a change does not affect it, e.g. when a modal is closed that was never registered as open
66+
- `openModalStack()` still returns the current stack synchronously, also directly after `setModalOpen()` was called
67+
- `<CodeEditor />`
68+
- `enableTab` overrides the Tab behaviour derived from `mode` and `tabIntentStyle` now, if it is set
69+
- `enableTab={false}` lets Tab always move the focus, even for modes with tab indentation
70+
- if it is not set, Tab is still handled as indentation for modes with `tabIntentStyle="tab"`
71+
- `<AutoSuggestion />`
72+
- Tab is only handled in the editor if `useTabForCompletions` is enabled, as side effect it currently also enables tab indentation
4773
- `<StringPreviewContentBlobToggler />`
4874
- `allowedHtmlElementsInPreview` option is set to inline elements on default
4975
- uses now the `Markdown.cutOff` property
@@ -61,6 +87,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
6187
- `show={"print"}` and `hide={"screen"}` content is still accessible by screen readers
6288
- `<Label />`
6389
- `tooltip` content is accessible via keyboard navigation
90+
- `<Card />`
91+
- fix color of first action button in info card
6492
- BOM issue on compressed stylesheet
6593
- first rule `selector` becomes `BOM:selector` that is valid but will never apply
6694
- we fixed this problem by adding a dummy rule as first rule

‎package.json‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,18 +191,15 @@
191191
},
192192
"lint-staged": {
193193
"*.(json|md)": [
194-
"prettier --write",
195-
"git add"
194+
"prettier --write"
196195
],
197196
"*.(js|ts|tsx)": [
198197
"eslint --fix",
199-
"prettier --write",
200-
"git add"
198+
"prettier --write"
201199
],
202200
"*.(scss)": [
203201
"stylelint --fix",
204-
"prettier --write",
205-
"git add"
202+
"prettier --write"
206203
]
207204
},
208205
"jest": {

‎src/common/utils/truncateMarkdownDisplay.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface MarkdownWithCutOffProps extends Omit<MarkdownProps, "cutOff"> {
88
cutOff: NonNullable<MarkdownProps["cutOff"]>;
99
}
1010

11-
interface TruncateMarkdownDisplayType {
11+
export interface TruncateMarkdownDisplayType {
1212
(
1313
/**
1414
* Markdown element with mandatory `cutOff` property.

‎src/components/AutoSuggestion/ExtendedCodeEditor.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const ExtendedCodeEditor = ({
7272
multiline = false,
7373
initialValue = "",
7474
onKeyDown,
75-
enableTab = false,
75+
enableTab,
7676
mode,
7777
setCM,
7878
onFocusChange,

‎src/components/AutoSuggestion/tests/AutoSuggestion.test.tsx‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
44

55
import "@testing-library/jest-dom";
66

7-
import AutoSuggestion, { AutoSuggestionProps } from "../AutoSuggestion";
7+
import CodeAutocompleteField, { CodeAutocompleteFieldProps } from "../AutoSuggestion";
88

9-
describe("AutoSuggestion", () => {
10-
let props: AutoSuggestionProps;
9+
describe("CodeAutocompleteField", () => {
10+
let props: CodeAutocompleteFieldProps;
1111

1212
beforeAll(() => {
1313
document.createRange = () => {
@@ -42,24 +42,24 @@ describe("AutoSuggestion", () => {
4242
});
4343

4444
it("should render properly", () => {
45-
const { container } = render(<AutoSuggestion {...props} />);
45+
const { container } = render(<CodeAutocompleteField {...props} />);
4646
expect(container).not.toBeEmptyDOMElement();
4747
});
4848

4949
it("should set label prop properly", () => {
50-
const { getByText } = render(<AutoSuggestion {...props} />);
50+
const { getByText } = render(<CodeAutocompleteField {...props} />);
5151
expect(getByText(props.label!)).toBeTruthy();
5252
});
5353

5454
it.each([false, true])("updates the container height (multiline: %s)", (multiline) => {
55-
const { container, rerender } = render(<AutoSuggestion {...props} multiline={multiline} height={120} />);
55+
const { container, rerender } = render(<CodeAutocompleteField {...props} multiline={multiline} height={120} />);
5656
const editorContainer = container.querySelector<HTMLElement>(".eccgui-codeeditor");
5757
expect(editorContainer).toHaveStyle({ height: "120px" });
5858

59-
rerender(<AutoSuggestion {...props} multiline={multiline} height="10rem" />);
59+
rerender(<CodeAutocompleteField {...props} multiline={multiline} height="10rem" />);
6060
expect(editorContainer).toHaveStyle({ height: "10rem" });
6161

62-
rerender(<AutoSuggestion {...props} multiline={multiline} />);
62+
rerender(<CodeAutocompleteField {...props} multiline={multiline} />);
6363
expect(editorContainer?.style.height).toBe("");
6464
});
6565

@@ -72,7 +72,7 @@ describe("AutoSuggestion", () => {
7272
"separates closing suggestions from Escape then Tab navigation ($multiline, $useTabForCompletions)",
7373
async ({ multiline, useTabForCompletions }) => {
7474
render(
75-
<AutoSuggestion
75+
<CodeAutocompleteField
7676
{...props}
7777
initialValue="value"
7878
mode="json"
@@ -106,6 +106,13 @@ describe("AutoSuggestion", () => {
106106
expect(editor).toHaveFocus();
107107
expect(editor.textContent).toBe(initialContent);
108108

109+
if (!useTabForCompletions) {
110+
// Tab is not handled by the editor and stays available for focus navigation.
111+
expect(fireEvent.keyDown(editor, { key: "Tab", code: "Tab", keyCode: 9 })).toBe(true);
112+
expect(editor.textContent).toBe(initialContent);
113+
return;
114+
}
115+
109116
expect(fireEvent.keyDown(editor, { key: "Tab", code: "Tab", keyCode: 9 })).toBe(false);
110117
const contentAfterIndent = editor.textContent;
111118
expect(contentAfterIndent).not.toBe(initialContent);

‎src/components/Card/card.scss‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ $eccgui-size-card-spacing: $eccgui-size-typo-base !default;
226226
@extend .#{$ns}-intent-success;
227227
}
228228
&.#{$eccgui}-intent--info > .#{$eccgui}-button:first-child {
229-
@extend .#{$ns}-intent-primary;
229+
@extend .#{$ns}-intent-accent;
230230
}
231231
&.#{$eccgui}-intent--warning > .#{$eccgui}-button:first-child {
232232
@extend .#{$ns}-intent-warning;

‎src/components/Dialog/AlertDialog.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Definitions as IntentStates, IntentTypes } from "../../common/Intent";
44

55
import SimpleDialog, { SimpleDialogProps } from "./SimpleDialog";
66

7-
export interface AlertDialogProps extends Omit<SimpleDialogProps, "intent"> {
7+
export interface AlertDialogProps extends Omit<SimpleDialogProps, "intent" | "role"> {
88
/**
99
* set to true if alert dialog displays a success message
1010
*/
@@ -21,7 +21,8 @@ export interface AlertDialogProps extends Omit<SimpleDialogProps, "intent"> {
2121

2222
/**
2323
* Special element to display alert notification in modal dialogs.
24-
* Inherits all properties from `SimpleDialog`, except `intent`.
24+
* Inherits all properties from `SimpleDialog`, except `intent` and `role`.
25+
* If `title`, `aria-label` nor a `aria-labelledby` is given then the alert level automatically used as fallback for `aria-label`.
2526
*/
2627
export const AlertDialog = ({
2728
children,
@@ -42,7 +43,7 @@ export const AlertDialog = ({
4243
}
4344

4445
return (
45-
<SimpleDialog size="tiny" preventSimpleClosing={true} intent={intentLevel} {...otherProps}>
46+
<SimpleDialog role="alertdialog" size="tiny" preventSimpleClosing={true} intent={intentLevel} {...otherProps}>
4647
{children}
4748
</SimpleDialog>
4849
);

‎src/components/Dialog/Modal.tsx‎

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import {
33
Classes as BlueprintClassNames,
4+
DialogProps as BlueprintDialogProps,
45
Overlay2 as BlueprintOverlay,
56
Overlay2Props as BlueprintOverlayProps,
67
} from "@blueprintjs/core";
@@ -11,9 +12,10 @@ import { CLASSPREFIX as eccgui } from "../../configuration/constants";
1112
import { TestableComponent } from "../interfaces";
1213

1314
import { Card } from "./../Card";
14-
import { ModalContext } from "./ModalContext";
15+
import { isModalContextProvided, ModalContext } from "./ModalContext";
1516

16-
export interface ModalProps extends TestableComponent, BlueprintOverlayProps {
17+
export interface ModalProps
18+
extends TestableComponent, BlueprintOverlayProps, Pick<BlueprintDialogProps, "role" | "aria-describedby"> {
1719
children: React.ReactNode | React.ReactNode[];
1820
/**
1921
* A space-delimited list of class names to pass along to the BlueprintJS `Overlay` element that is used to create the modal.
@@ -52,6 +54,18 @@ export interface ModalProps extends TestableComponent, BlueprintOverlayProps {
5254
* Prevents that pan and zooming actions of an existing react-flow instance are triggered while this Modal is open.
5355
*/
5456
preventReactFlowEvents?: boolean;
57+
/**
58+
* ID of the element that contains title or label text for this dialog.
59+
* If given then `aria-label` is ignored.
60+
* In v27 it will be enforced that `aria-label` or `aria-labelledby` is set.
61+
*/
62+
"aria-labelledby"?: string;
63+
/**
64+
* Set this if there is no visible title element that is used for `aria-labelledby`.
65+
* Property is ignored if `aria-labelledby` is given.
66+
* In v27 it will be enforced that `aria-label` or `aria-labelledby` is set.
67+
*/
68+
"aria-label"?: string;
5569
}
5670

5771
export type ModalSize = "tiny" | "small" | "regular" | "large" | "xlarge" | "fullscreen";
@@ -78,6 +92,10 @@ export const Modal = ({
7892
"data-test-id": dataTestId,
7993
"data-testid": dataTestid,
8094
modalId,
95+
role = "dialog",
96+
"aria-labelledby": ariaLabelledby,
97+
"aria-describedby": ariaDescribedby,
98+
"aria-label": ariaLabel,
8199
preventReactFlowEvents = true,
82100
...otherProps
83101
}: ModalProps) => {
@@ -93,7 +111,14 @@ export const Modal = ({
93111
};
94112
}, []);
95113

114+
// always remove the role if there is no explanation
115+
const modalRole = ariaLabel || ariaLabelledby ? role : undefined;
116+
96117
React.useEffect(() => {
118+
if (!modalRole && otherProps.isOpen) {
119+
// eslint-disable-next-line no-console
120+
console.warn(`role=${role} removed from modal because aria-label nor aria-labelledby is available.`);
121+
}
97122
modalContext.setModalOpen(uniqueModalId.current, otherProps.isOpen);
98123
}, [otherProps.isOpen]);
99124

@@ -140,6 +165,23 @@ export const Modal = ({
140165
}
141166
};
142167

168+
// Only the modal that was opened last constrains assistive technologies to its contents.
169+
// Without a provided ModalContext the modals do not know about each other, then each of them
170+
// has to consider itself as the one that constrains.
171+
const openModalStack = modalContext.openModalStack() ?? [];
172+
const isTopMostModal = isModalContextProvided(modalContext)
173+
? openModalStack[openModalStack.length - 1] === uniqueModalId.current
174+
: true;
175+
176+
const modalAriaAttributes = {
177+
role: modalRole,
178+
"aria-label": !ariaLabelledby ? ariaLabel : undefined,
179+
"aria-labelledby": ariaLabelledby,
180+
"aria-describedby": ariaDescribedby,
181+
// modality can only be expressed together with a dialog role
182+
"aria-modal": modalRole ? isTopMostModal : undefined,
183+
};
184+
143185
return (
144186
<BlueprintOverlay
145187
{...otherProps}
@@ -158,15 +200,16 @@ export const Modal = ({
158200
className={BlueprintClassNames.DIALOG_CONTAINER}
159201
// this is a workaround because data attribute on SimpleDialog is not correctly routed to the overlay by blueprint js
160202
{...{ "data-test-id": dataTestId ?? "simpleDialogWidget", "data-testid": dataTestid }}
161-
{...focusableProps}
162203
tabIndex={0}
204+
{...focusableProps}
163205
>
164206
<section
165207
className={
166208
`${eccgui}-dialog__wrapper` +
167209
(typeof size === "string" ? ` ${eccgui}-dialog__wrapper--` + size : "") +
168210
(className ? " " + className : "")
169211
}
212+
{...modalAriaAttributes}
170213
>
171214
{alteredChildren}
172215
</section>

0 commit comments

Comments
 (0)