@kaizen/components 1.42.7 → 1.44.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.
Files changed (36) hide show
  1. package/dist/cjs/LikertScaleLegacy/LikertScaleLegacy.cjs +6 -4
  2. package/dist/cjs/LikertScaleLegacy/LikertScaleLegacy.module.scss.cjs +2 -0
  3. package/dist/cjs/Modal/ConfirmationModal/ConfirmationModal.cjs +4 -2
  4. package/dist/cjs/Modal/ContextModal/ContextModal.cjs +4 -2
  5. package/dist/cjs/Modal/GenericModal/GenericModal.cjs +12 -9
  6. package/dist/cjs/Modal/InputEditModal/InputEditModal.cjs +4 -2
  7. package/dist/cjs/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.cjs +5 -1
  8. package/dist/cjs/index.css +3 -3
  9. package/dist/esm/LikertScaleLegacy/LikertScaleLegacy.mjs +6 -4
  10. package/dist/esm/LikertScaleLegacy/LikertScaleLegacy.module.scss.mjs +2 -0
  11. package/dist/esm/Modal/ConfirmationModal/ConfirmationModal.mjs +4 -2
  12. package/dist/esm/Modal/ContextModal/ContextModal.mjs +4 -2
  13. package/dist/esm/Modal/GenericModal/GenericModal.mjs +12 -9
  14. package/dist/esm/Modal/InputEditModal/InputEditModal.mjs +4 -2
  15. package/dist/esm/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.mjs +5 -1
  16. package/dist/esm/index.css +3 -3
  17. package/dist/styles.css +1 -1
  18. package/dist/types/LikertScaleLegacy/LikertScaleLegacy.d.ts +3 -2
  19. package/dist/types/LikertScaleLegacy/types.d.ts +1 -0
  20. package/dist/types/Modal/ConfirmationModal/ConfirmationModal.d.ts +4 -1
  21. package/dist/types/Modal/ContextModal/ContextModal.d.ts +4 -1
  22. package/dist/types/Modal/GenericModal/GenericModal.d.ts +4 -1
  23. package/dist/types/Modal/InputEditModal/InputEditModal.d.ts +4 -1
  24. package/package.json +3 -3
  25. package/src/LikertScaleLegacy/LikertScaleLegacy.module.scss +66 -17
  26. package/src/LikertScaleLegacy/LikertScaleLegacy.tsx +6 -1
  27. package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.stickersheet.stories.tsx +26 -4
  28. package/src/LikertScaleLegacy/types.ts +2 -0
  29. package/src/Modal/ConfirmationModal/ConfirmationModal.tsx +5 -0
  30. package/src/Modal/ContextModal/ContextModal.tsx +5 -0
  31. package/src/Modal/GenericModal/GenericModal.tsx +18 -10
  32. package/src/Modal/GenericModal/_docs/GenericModal.spec.stories.tsx +124 -0
  33. package/src/Modal/InputEditModal/InputEditModal.tsx +5 -0
  34. package/src/Modal/InputEditModal/_docs/InputEditModal.mdx +11 -0
  35. package/src/Modal/InputEditModal/_docs/InputEditModal.stories.tsx +63 -1
  36. package/src/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.tsx +1 -0
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { Scale, ScaleItem } from "./types";
2
+ import { Scale, ScaleItem, ColorSchema } from "./types";
3
3
  export type LikertScaleProps = {
4
4
  labelId: string;
5
5
  scale: Scale;
@@ -10,6 +10,7 @@ export type LikertScaleProps = {
10
10
  automationId?: string;
11
11
  "data-testid"?: string;
12
12
  reversed?: boolean;
13
+ colorSchema?: ColorSchema | "classical";
13
14
  validationMessage?: string;
14
15
  status?: "default" | "error";
15
16
  onSelect: (value: ScaleItem | null) => void;
@@ -18,4 +19,4 @@ export type LikertScaleProps = {
18
19
  * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082060201/Likert+Scale Guidance} |
19
20
  * {@link https://cultureamp.design/?path=/docs/components-likertscalelegacy--docs Storybook}
20
21
  */
21
- export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, }: LikertScaleProps) => JSX.Element;
22
+ export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, colorSchema, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, }: LikertScaleProps) => JSX.Element;
@@ -3,4 +3,5 @@ export type ScaleItem = {
3
3
  value: ScaleValue;
4
4
  label: string;
5
5
  };
6
+ export type ColorSchema = "classical" | "blue";
6
7
  export type Scale = ScaleItem[];
@@ -13,6 +13,9 @@ export type ConfirmationModalProps = {
13
13
  title: string;
14
14
  onConfirm?: () => void;
15
15
  onDismiss: () => void;
16
+ /** A callback that is triggered after the modal is opened. */
17
+ onAfterEnter?: () => void;
18
+ /** A callback that is triggered after the modal is closed. */
16
19
  onAfterLeave?: () => void;
17
20
  confirmLabel?: string;
18
21
  dismissLabel?: string;
@@ -32,7 +35,7 @@ type Mood = "positive" | "informative" | "negative" | "cautionary" | "assertive"
32
35
  * {@link https://cultureamp.design/?path=/docs/components-modals-confirmationmodal--docs Storybook}
33
36
  */
34
37
  export declare const ConfirmationModal: {
35
- ({ isOpen, isProminent, unpadded, mood, title, onConfirm, onAfterLeave, confirmLabel, dismissLabel, confirmWorking, onDismiss: propsOnDismiss, children, ...props }: ConfirmationModalProps): JSX.Element;
38
+ ({ isOpen, isProminent, unpadded, mood, title, onConfirm, onAfterLeave, onAfterEnter, confirmLabel, dismissLabel, confirmWorking, onDismiss: propsOnDismiss, children, ...props }: ConfirmationModalProps): JSX.Element;
36
39
  displayName: string;
37
40
  };
38
41
  export {};
@@ -16,6 +16,9 @@ export type ContextModalProps = Readonly<{
16
16
  title: string;
17
17
  onConfirm?: () => void;
18
18
  onDismiss: () => void;
19
+ /** A callback that is triggered after the modal is opened. */
20
+ onAfterEnter?: () => void;
21
+ /** A callback that is triggered after the modal is closed. */
19
22
  onAfterLeave?: () => void;
20
23
  confirmLabel?: string;
21
24
  confirmWorking?: {
@@ -36,6 +39,6 @@ export type ContextModalProps = Readonly<{
36
39
  * {@link https://cultureamp.design/?path=/docs/components-modals--contextmodal--docs Storybook}
37
40
  */
38
41
  export declare const ContextModal: {
39
- ({ isOpen, unpadded, layout, title, onConfirm, onDismiss: propsOnDismiss, onAfterLeave, confirmLabel, confirmWorking, renderBackground, children, contentHeader, image, secondaryLabel, onSecondaryAction, ...props }: ContextModalProps): JSX.Element;
42
+ ({ isOpen, unpadded, layout, title, onConfirm, onDismiss: propsOnDismiss, onAfterLeave, onAfterEnter, confirmLabel, confirmWorking, renderBackground, children, contentHeader, image, secondaryLabel, onSecondaryAction, ...props }: ContextModalProps): JSX.Element;
40
43
  displayName: string;
41
44
  };
@@ -6,9 +6,12 @@ export type GenericModalProps = {
6
6
  focusLockDisabled?: boolean;
7
7
  onEscapeKeyup?: (event: KeyboardEvent) => void;
8
8
  onOutsideModalClick?: (event: React.MouseEvent) => void;
9
+ /** A callback that is triggered after the modal is opened. */
10
+ onAfterEnter?: () => void;
11
+ /** A callback that is triggered after the modal is closed. */
9
12
  onAfterLeave?: () => void;
10
13
  };
11
14
  export declare const GenericModal: {
12
- ({ id: propsId, children, isOpen, focusLockDisabled, onEscapeKeyup, onOutsideModalClick, onAfterLeave: propsOnAfterLeave, }: GenericModalProps): JSX.Element;
15
+ ({ id: propsId, children, isOpen, focusLockDisabled, onEscapeKeyup, onOutsideModalClick, onAfterEnter, onAfterLeave: propsOnAfterLeave, }: GenericModalProps): JSX.Element;
13
16
  displayName: string;
14
17
  };
@@ -7,6 +7,9 @@ export type InputEditModalProps = {
7
7
  onSubmit: () => void;
8
8
  onSecondaryAction?: () => void;
9
9
  onDismiss: () => void;
10
+ /** A callback that is triggered after the modal is opened. */
11
+ onAfterEnter?: () => void;
12
+ /** A callback that is triggered after the modal is closed. */
10
13
  onAfterLeave?: () => void;
11
14
  localeDirection?: "rtl" | "ltr";
12
15
  submitLabel?: string;
@@ -27,6 +30,6 @@ export type InputEditModalProps = {
27
30
  * {@link https://cultureamp.design/?path=/docs/components-modals-inputeditmodal--docs Storybook}
28
31
  */
29
32
  export declare const InputEditModal: {
30
- ({ isOpen, mood, title, onSubmit, onSecondaryAction, onAfterLeave, localeDirection, submitLabel, dismissLabel, secondaryLabel, submitWorking, children, unpadded, onDismiss: propsOnDismiss, ...props }: InputEditModalProps): JSX.Element;
33
+ ({ isOpen, mood, title, onSubmit, onSecondaryAction, onAfterLeave, localeDirection, submitLabel, dismissLabel, secondaryLabel, submitWorking, children, unpadded, onDismiss: propsOnDismiss, onAfterEnter, ...props }: InputEditModalProps): JSX.Element;
31
34
  displayName: string;
32
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "1.42.7",
3
+ "version": "1.44.0",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -108,8 +108,8 @@
108
108
  "ts-node": "^10.9.2",
109
109
  "ts-patch": "^3.1.2",
110
110
  "typescript-transform-paths": "^3.4.7",
111
- "@kaizen/design-tokens": "10.3.20",
112
- "@kaizen/tailwind": "1.2.6"
111
+ "@kaizen/tailwind": "1.2.6",
112
+ "@kaizen/design-tokens": "10.3.20"
113
113
  },
114
114
  "peerDependencies": {
115
115
  "@cultureamp/i18n-react-intl": "^2.5.5",
@@ -23,11 +23,17 @@ $block-height: 35px;
23
23
  $desktop-rater-width: 220px;
24
24
  $desktop-rater-height: 63px;
25
25
 
26
- $first: $color-yellow-300;
27
- $second: $color-yellow-400;
28
- $third: $color-orange-400;
29
- $fourth: $color-orange-500;
30
- $fifth: $color-red-500;
26
+ $classical-first: $color-yellow-300;
27
+ $classical-second: $color-yellow-400;
28
+ $classical-third: $color-orange-400;
29
+ $classical-fourth: $color-orange-500;
30
+ $classical-fifth: $color-red-500;
31
+
32
+ $blue-first: $color-blue-100;
33
+ $blue-second: $color-blue-200;
34
+ $blue-third: $color-blue-300;
35
+ $blue-fourth: $color-blue-400;
36
+ $blue-fifth: $color-blue-500;
31
37
 
32
38
  @mixin pop {
33
39
  -webkit-animation: pop cubic-bezier(0, 0.94, 0.32, 1) 0.7s 1;
@@ -212,45 +218,88 @@ $fifth: $color-red-500;
212
218
  left: 100%;
213
219
  }
214
220
 
215
- &.suggested,
216
- &.selected {
221
+ &.suggested.classicalColorSchema,
222
+ &.selected.classicalColorSchema {
217
223
  .field1 {
218
- background-color: $first;
224
+ background-color: $classical-first;
219
225
 
220
226
  &::after {
221
- background-color: $first;
227
+ background-color: $classical-first;
222
228
  }
223
229
  }
224
230
 
225
231
  .field2 {
226
- background-color: $second;
232
+ background-color: $classical-second;
227
233
 
228
234
  &::after {
229
- background-color: $second;
235
+ background-color: $classical-second;
230
236
  }
231
237
  }
232
238
 
233
239
  .field3 {
234
- background-color: $third;
240
+ background-color: $classical-third;
235
241
 
236
242
  &::after {
237
- background-color: $third;
243
+ background-color: $classical-third;
238
244
  }
239
245
  }
240
246
 
241
247
  .field4 {
242
- background-color: $fourth;
248
+ background-color: $classical-fourth;
243
249
 
244
250
  &::after {
245
- background-color: $fourth;
251
+ background-color: $classical-fourth;
246
252
  }
247
253
  }
248
254
 
249
255
  .field5 {
250
- background-color: $fifth;
256
+ background-color: $classical-fifth;
251
257
 
252
258
  &::after {
253
- background-color: $fifth;
259
+ background-color: $classical-fifth;
260
+ }
261
+ }
262
+ }
263
+
264
+ &.suggested.blueColorSchema,
265
+ &.selected.blueColorSchema {
266
+ .field1 {
267
+ background-color: $blue-first;
268
+
269
+ &::after {
270
+ background-color: $blue-first;
271
+ }
272
+ }
273
+
274
+ .field2 {
275
+ background-color: $blue-second;
276
+
277
+ &::after {
278
+ background-color: $blue-second;
279
+ }
280
+ }
281
+
282
+ .field3 {
283
+ background-color: $blue-third;
284
+
285
+ &::after {
286
+ background-color: $blue-third;
287
+ }
288
+ }
289
+
290
+ .field4 {
291
+ background-color: $blue-fourth;
292
+
293
+ &::after {
294
+ background-color: $blue-fourth;
295
+ }
296
+ }
297
+
298
+ .field5 {
299
+ background-color: $blue-fifth;
300
+
301
+ &::after {
302
+ background-color: $blue-fifth;
254
303
  }
255
304
  }
256
305
  }
@@ -3,7 +3,7 @@ import classnames from "classnames"
3
3
  import { FieldMessage } from "~components/FieldMessage"
4
4
  import { CheckIcon } from "~components/Icon"
5
5
  import { Text } from "~components/Text"
6
- import { ScaleValue, Scale, ScaleItem } from "./types"
6
+ import { ScaleValue, Scale, ScaleItem, ColorSchema } from "./types"
7
7
  import determineSelectionFromKeyPress from "./utils/determineSelectionFromKeyPress"
8
8
  import styles from "./LikertScaleLegacy.module.scss"
9
9
 
@@ -22,6 +22,7 @@ export type LikertScaleProps = {
22
22
  automationId?: string
23
23
  "data-testid"?: string
24
24
  reversed?: boolean
25
+ colorSchema?: ColorSchema | "classical"
25
26
  validationMessage?: string
26
27
  status?: "default" | "error"
27
28
  onSelect: (value: ScaleItem | null) => void
@@ -39,6 +40,7 @@ export const LikertScaleLegacy = ({
39
40
  scale,
40
41
  selectedItem,
41
42
  reversed,
43
+ colorSchema = "classical",
42
44
  "data-testid": dataTestId,
43
45
  onSelect,
44
46
  validationMessage,
@@ -156,6 +158,9 @@ export const LikertScaleLegacy = ({
156
158
  <div
157
159
  className={classnames(
158
160
  styles.likertItem,
161
+ colorSchema == "blue"
162
+ ? styles.blueColorSchema
163
+ : styles.classicalColorSchema,
159
164
  styles[`likertItem${item.value}`],
160
165
  isSelected && styles.selected,
161
166
  isSuggested && styles.suggested,
@@ -43,7 +43,7 @@ const scale: Scale = [
43
43
  ]
44
44
 
45
45
  const StickerSheetTemplate: StickerSheetStory = {
46
- render: ({ isReversed }) => (
46
+ render: ({ isReversed, colorSchema }) => (
47
47
  <StickerSheet isReversed={isReversed}>
48
48
  <StickerSheet.Body>
49
49
  <StickerSheet.Row rowTitle="Not rated">
@@ -53,6 +53,7 @@ const StickerSheetTemplate: StickerSheetStory = {
53
53
  selectedItem={scale[0]}
54
54
  onSelect={(): void => undefined}
55
55
  reversed={isReversed}
56
+ colorSchema={colorSchema}
56
57
  />
57
58
  </StickerSheet.Row>
58
59
  <StickerSheet.Row rowTitle="Strongly disagree">
@@ -62,6 +63,7 @@ const StickerSheetTemplate: StickerSheetStory = {
62
63
  selectedItem={scale[1]}
63
64
  onSelect={(): void => undefined}
64
65
  reversed={isReversed}
66
+ colorSchema={colorSchema}
65
67
  />
66
68
  </StickerSheet.Row>
67
69
  <StickerSheet.Row rowTitle="Disagree">
@@ -71,6 +73,7 @@ const StickerSheetTemplate: StickerSheetStory = {
71
73
  selectedItem={scale[2]}
72
74
  onSelect={(): void => undefined}
73
75
  reversed={isReversed}
76
+ colorSchema={colorSchema}
74
77
  />
75
78
  </StickerSheet.Row>
76
79
  <StickerSheet.Row rowTitle="Neither agree or disagree">
@@ -80,6 +83,7 @@ const StickerSheetTemplate: StickerSheetStory = {
80
83
  selectedItem={scale[3]}
81
84
  onSelect={(): void => undefined}
82
85
  reversed={isReversed}
86
+ colorSchema={colorSchema}
83
87
  />
84
88
  </StickerSheet.Row>
85
89
  <StickerSheet.Row rowTitle="Agree">
@@ -89,6 +93,7 @@ const StickerSheetTemplate: StickerSheetStory = {
89
93
  selectedItem={scale[4]}
90
94
  onSelect={(): void => undefined}
91
95
  reversed={isReversed}
96
+ colorSchema={colorSchema}
92
97
  />
93
98
  </StickerSheet.Row>
94
99
  <StickerSheet.Row rowTitle="Strongly agree">
@@ -98,6 +103,7 @@ const StickerSheetTemplate: StickerSheetStory = {
98
103
  selectedItem={scale[5]}
99
104
  onSelect={(): void => undefined}
100
105
  reversed={isReversed}
106
+ colorSchema={colorSchema}
101
107
  />
102
108
  </StickerSheet.Row>
103
109
  <StickerSheet.Row rowTitle="Validation">
@@ -107,6 +113,7 @@ const StickerSheetTemplate: StickerSheetStory = {
107
113
  selectedItem={scale[0]}
108
114
  onSelect={(): void => undefined}
109
115
  reversed={isReversed}
116
+ colorSchema={colorSchema}
110
117
  validationMessage="Error message here"
111
118
  status="error"
112
119
  />
@@ -118,18 +125,33 @@ const StickerSheetTemplate: StickerSheetStory = {
118
125
 
119
126
  export const StickerSheetDefault: StickerSheetStory = {
120
127
  ...StickerSheetTemplate,
121
- name: "Sticker Sheet (Default)",
128
+ name: "Sticker Sheet (Default - Classical)",
122
129
  }
123
130
 
124
- export const StickerSheetReversed: StickerSheetStory = {
131
+ export const StickerBlueSheetDefault: StickerSheetStory = {
125
132
  ...StickerSheetTemplate,
126
- name: "Sticker Sheet (Reversed)",
133
+ name: "Sticker Sheet (Blue)",
134
+ args: { colorSchema: "blue" },
135
+ }
136
+
137
+ export const StickerSheetClassicalReversed: StickerSheetStory = {
138
+ ...StickerSheetTemplate,
139
+ name: "Sticker Sheet (Classical Reversed)",
127
140
  parameters: {
128
141
  backgrounds: { default: "Purple 700" },
129
142
  },
130
143
  args: { isReversed: true },
131
144
  }
132
145
 
146
+ export const StickerSheetBlueReversed: StickerSheetStory = {
147
+ ...StickerSheetTemplate,
148
+ name: "Sticker Sheet (Blue Reversed)",
149
+ parameters: {
150
+ backgrounds: { default: "Purple 700" },
151
+ },
152
+ args: { isReversed: true, colorSchema: "blue" },
153
+ }
154
+
133
155
  export const StickerSheetRTL: StickerSheetStory = {
134
156
  ...StickerSheetTemplate,
135
157
  name: "Sticker Sheet (RTL)",
@@ -5,4 +5,6 @@ export type ScaleItem = {
5
5
  label: string
6
6
  }
7
7
 
8
+ export type ColorSchema = "classical" | "blue"
9
+
8
10
  export type Scale = ScaleItem[]
@@ -39,6 +39,9 @@ export type ConfirmationModalProps = {
39
39
  title: string
40
40
  onConfirm?: () => void
41
41
  onDismiss: () => void
42
+ /** A callback that is triggered after the modal is opened. */
43
+ onAfterEnter?: () => void
44
+ /** A callback that is triggered after the modal is closed. */
42
45
  onAfterLeave?: () => void
43
46
  confirmLabel?: string
44
47
  dismissLabel?: string
@@ -99,6 +102,7 @@ export const ConfirmationModal = ({
99
102
  title,
100
103
  onConfirm,
101
104
  onAfterLeave,
105
+ onAfterEnter,
102
106
  confirmLabel = "Confirm",
103
107
  dismissLabel = "Cancel",
104
108
  confirmWorking,
@@ -134,6 +138,7 @@ export const ConfirmationModal = ({
134
138
  onEscapeKeyup={onDismiss}
135
139
  onOutsideModalClick={onDismiss}
136
140
  onAfterLeave={onAfterLeave}
141
+ onAfterEnter={onAfterEnter}
137
142
  >
138
143
  <div className={styles.modal} data-modal {...props}>
139
144
  <ModalHeader onDismiss={onDismiss}>
@@ -32,6 +32,9 @@ export type ContextModalProps = Readonly<
32
32
  title: string
33
33
  onConfirm?: () => void
34
34
  onDismiss: () => void
35
+ /** A callback that is triggered after the modal is opened. */
36
+ onAfterEnter?: () => void
37
+ /** A callback that is triggered after the modal is closed. */
35
38
  onAfterLeave?: () => void
36
39
  confirmLabel?: string
37
40
  confirmWorking?: { label: string; labelHidden?: boolean }
@@ -59,6 +62,7 @@ export const ContextModal = ({
59
62
  onConfirm,
60
63
  onDismiss: propsOnDismiss,
61
64
  onAfterLeave,
65
+ onAfterEnter,
62
66
  confirmLabel = "Confirm",
63
67
  confirmWorking,
64
68
  renderBackground,
@@ -100,6 +104,7 @@ export const ContextModal = ({
100
104
  onEscapeKeyup={onDismiss}
101
105
  onOutsideModalClick={onDismiss}
102
106
  onAfterLeave={onAfterLeave}
107
+ onAfterEnter={onAfterEnter}
103
108
  >
104
109
  <div className={styles.modal} data-modal {...props}>
105
110
  {renderBackground && renderBackground()}
@@ -13,6 +13,9 @@ export type GenericModalProps = {
13
13
  focusLockDisabled?: boolean
14
14
  onEscapeKeyup?: (event: KeyboardEvent) => void
15
15
  onOutsideModalClick?: (event: React.MouseEvent) => void
16
+ /** A callback that is triggered after the modal is opened. */
17
+ onAfterEnter?: () => void
18
+ /** A callback that is triggered after the modal is closed. */
16
19
  onAfterLeave?: () => void
17
20
  }
18
21
 
@@ -23,6 +26,7 @@ export const GenericModal = ({
23
26
  focusLockDisabled,
24
27
  onEscapeKeyup,
25
28
  onOutsideModalClick,
29
+ onAfterEnter,
26
30
  onAfterLeave: propsOnAfterLeave,
27
31
  }: GenericModalProps): JSX.Element => {
28
32
  const reactId = useId()
@@ -50,18 +54,19 @@ export const GenericModal = ({
50
54
  }
51
55
  }
52
56
 
53
- const focusAccessibleLabel = (): void => {
54
- if (modalLayer) {
55
- const labelElement: HTMLElement | null =
56
- document.getElementById(labelledByID)
57
- if (labelElement) {
58
- labelElement.focus()
59
- }
57
+ const focusOnAccessibleLabel = (): void => {
58
+ // Check if focus already exists within the modal
59
+ if (modalLayer?.contains(document.activeElement)) {
60
+ return
60
61
  }
62
+
63
+ const labelElement: HTMLElement | null =
64
+ document.getElementById(labelledByID)
65
+
66
+ labelElement?.focus()
61
67
  }
62
68
 
63
69
  const a11yWarn = (): void => {
64
- if (!modalLayer) return
65
70
  // Ensure that consumers have provided an element that labels the modal
66
71
  // to meet ARIA accessibility guidelines.
67
72
  if (!document.getElementById(labelledByID)) {
@@ -86,8 +91,11 @@ export const GenericModal = ({
86
91
 
87
92
  const onAfterEnterHandler = (): void => {
88
93
  scrollModalToTop()
89
- focusAccessibleLabel()
90
- a11yWarn()
94
+ if (modalLayer) {
95
+ onAfterEnter?.()
96
+ focusOnAccessibleLabel()
97
+ a11yWarn()
98
+ }
91
99
  }
92
100
 
93
101
  const onBeforeEnterHandler = (): void => {
@@ -0,0 +1,124 @@
1
+ import React from "react"
2
+ import { action } from "@storybook/addon-actions"
3
+ import { Meta, StoryObj } from "@storybook/react"
4
+ import { expect, userEvent, within, waitFor } from "@storybook/test"
5
+
6
+ import {
7
+ GenericModal,
8
+ ModalAccessibleLabel,
9
+ ModalBody,
10
+ ModalHeader,
11
+ } from "../index"
12
+
13
+ const meta: Meta<typeof GenericModal> = {
14
+ title: "Components/Modals/Generic Modal/Tests",
15
+ component: GenericModal,
16
+ }
17
+
18
+ export default meta
19
+
20
+ type Story = StoryObj<typeof GenericModal>
21
+
22
+ export const TestBase: Story = {
23
+ render: ({ isOpen: propsIsOpen, ...args }) => {
24
+ const [isOpen, setIsOpen] = React.useState<boolean>(propsIsOpen)
25
+ const handleDismiss = (): void => setIsOpen(false)
26
+
27
+ return (
28
+ <>
29
+ <button
30
+ type="button"
31
+ className="border border-gray-500"
32
+ onClick={() => setIsOpen(true)}
33
+ >
34
+ Open Modal
35
+ </button>
36
+ <GenericModal
37
+ {...args}
38
+ isOpen={isOpen}
39
+ onOutsideModalClick={handleDismiss}
40
+ onEscapeKeyup={handleDismiss}
41
+ id="GenericModalTestId"
42
+ >
43
+ <ModalHeader>
44
+ <ModalAccessibleLabel>Test Modal</ModalAccessibleLabel>
45
+ </ModalHeader>
46
+ <ModalBody>
47
+ <form>
48
+ <label htmlFor="modal-input-play-test">Add link</label>
49
+ <input type="text" id="modal-input-play-test" />
50
+ </form>
51
+ </ModalBody>
52
+ </GenericModal>
53
+ </>
54
+ )
55
+ },
56
+ play: async ({ canvasElement, step }) => {
57
+ const { getByRole } = within(canvasElement)
58
+
59
+ const openModalButton = getByRole("button", { name: "Open Modal" })
60
+
61
+ await step("Open modal", async () => {
62
+ await userEvent.click(openModalButton)
63
+ })
64
+
65
+ await step("Default focus is shifted to the Accessible title", async () => {
66
+ await waitFor(() => {
67
+ // document has to be use as Modal will append to document body
68
+ expect(document.activeElement).toHaveTextContent("Test Modal")
69
+ })
70
+ })
71
+ },
72
+ }
73
+
74
+ export const ModalAccessibleLabelRetainsFocus: Story = {
75
+ ...TestBase,
76
+ name: "ModalAccessibleLabel retains focus if onAfterEnter is called",
77
+ args: {
78
+ onAfterEnter: () => action("openCallBack"),
79
+ },
80
+ play: async ({ canvasElement, step }) => {
81
+ const { getByRole } = within(canvasElement)
82
+
83
+ const openModalButton = getByRole("button", { name: "Open Modal" })
84
+
85
+ await step("Open modal", async () => {
86
+ await userEvent.click(openModalButton)
87
+ })
88
+
89
+ await step("Accessible title still has focus", async () => {
90
+ await waitFor(() => {
91
+ expect(document.activeElement).toHaveTextContent("Test Modal")
92
+ })
93
+ })
94
+ },
95
+ }
96
+
97
+ export const TriggerOnAfterEnterFocus: Story = {
98
+ ...TestBase,
99
+ args: {
100
+ onAfterEnter: () =>
101
+ document.getElementById("modal-input-play-test")?.focus(),
102
+ },
103
+ name: "onAfterEnter can shift focus to internal elements of the modal",
104
+ play: async ({ canvasElement, step }) => {
105
+ const canvas = within(canvasElement)
106
+ const openModalButton = canvas.getByRole("button", { name: "Open Modal" })
107
+
108
+ await step("Open modal", async () => {
109
+ await userEvent.click(openModalButton)
110
+ })
111
+
112
+ await step("Expect activeElement to be the Input", async () => {
113
+ await waitFor(() => {
114
+ expect(document.activeElement).toHaveAccessibleName("Add link")
115
+ })
116
+ })
117
+
118
+ await step("Expect to be able to type without shifting focus", async () => {
119
+ await userEvent.keyboard(
120
+ "All lorem and no ipsum make dolar a dull boy..."
121
+ )
122
+ })
123
+ },
124
+ }
@@ -19,6 +19,9 @@ export type InputEditModalProps = {
19
19
  onSubmit: () => void
20
20
  onSecondaryAction?: () => void
21
21
  onDismiss: () => void
22
+ /** A callback that is triggered after the modal is opened. */
23
+ onAfterEnter?: () => void
24
+ /** A callback that is triggered after the modal is closed. */
22
25
  onAfterLeave?: () => void
23
26
  localeDirection?: "rtl" | "ltr"
24
27
  submitLabel?: string
@@ -51,6 +54,7 @@ export const InputEditModal = ({
51
54
  children,
52
55
  unpadded = false,
53
56
  onDismiss: propsOnDismiss,
57
+ onAfterEnter,
54
58
  ...props
55
59
  }: InputEditModalProps): JSX.Element => {
56
60
  const onDismiss = submitWorking ? undefined : propsOnDismiss
@@ -79,6 +83,7 @@ export const InputEditModal = ({
79
83
  isOpen={isOpen}
80
84
  onEscapeKeyup={onDismiss}
81
85
  onAfterLeave={onAfterLeave}
86
+ onAfterEnter={onAfterEnter}
82
87
  >
83
88
  <div className={styles.modal} dir={localeDirection} data-modal {...props}>
84
89
  <ModalHeader onDismiss={onDismiss}>
@@ -39,3 +39,14 @@ When your modal is preforming a destructive action eg. delete customer data.
39
39
  ### Unpadded
40
40
 
41
41
  <Canvas of={InputEditModalStories.Unpadded} />
42
+
43
+ ## onAfterEnter and shifting focus
44
+
45
+ It is an important accessibility consideration that any time we shift focus on the page, no important content is skipped that may provide context to assistive technologies. This is why the default behaviour for our modals is to shift focus to the accessible title.
46
+
47
+ There are instances, such as single input modals, where shifting focus may not impact the users context. In these instances, we can leverage the `onAfterEnter` callback to shift focus to an input.
48
+
49
+ <Canvas of={InputEditModalStories.OnAfterEnter} sourceState="shown" />
50
+
51
+ As both the button and input label have clear intent and the focus does not skip past crucial content, this should provide enough context for an end user.
52
+