@scality/core-ui 0.202.0 → 0.204.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 (38) hide show
  1. package/.github/workflows/github-pages.yml +4 -4
  2. package/.github/workflows/post-release.yml +25 -12
  3. package/__mocks__/fileMock.js +1 -1
  4. package/__mocks__/styleMock.js +1 -1
  5. package/__mocks__/uuid.js +1 -5
  6. package/dist/components/drawer/Drawer.component.d.ts +17 -0
  7. package/dist/components/drawer/Drawer.component.d.ts.map +1 -0
  8. package/dist/components/drawer/Drawer.component.js +132 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/next.d.ts +1 -0
  13. package/dist/next.d.ts.map +1 -1
  14. package/dist/next.js +1 -0
  15. package/dist/style/theme.d.ts +1 -0
  16. package/dist/style/theme.d.ts.map +1 -1
  17. package/dist/style/theme.js +1 -0
  18. package/dist/utils.d.ts.map +1 -1
  19. package/dist/utils.js +11 -16
  20. package/global-setup.js +1 -1
  21. package/jest.config.js +1 -1
  22. package/package.json +13 -3
  23. package/src/lib/components/drawer/Drawer.component.test.tsx +108 -0
  24. package/src/lib/components/drawer/Drawer.component.tsx +207 -0
  25. package/src/lib/index.ts +1 -0
  26. package/src/lib/next.ts +1 -0
  27. package/src/lib/style/theme.ts +1 -0
  28. package/src/lib/utils.test.ts +15 -12
  29. package/src/lib/utils.ts +15 -17
  30. package/src/lib/valalint/README.md +50 -0
  31. package/src/lib/valalint/index.js +49 -0
  32. package/src/lib/valalint/rules/modal-button-forbidden-label.js +87 -0
  33. package/src/lib/valalint/rules/modal-button-forbidden-label.test.js +157 -0
  34. package/src/lib/valalint/rules/no-raw-number-in-jsx.js +64 -0
  35. package/src/lib/valalint/rules/no-raw-number-in-jsx.test.js +237 -0
  36. package/src/lib/valalint/rules/technical-sentence-case.js +93 -0
  37. package/src/lib/valalint/rules/technical-sentence-case.test.js +167 -0
  38. package/stories/drawer.stories.tsx +135 -0
@@ -0,0 +1,207 @@
1
+ import { CSSProperties, type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import styled from 'styled-components';
4
+ import { spacing } from '../../spacing';
5
+ import { zIndex } from '../../style/theme';
6
+ import { getThemePropSelector } from '../../utils';
7
+ import { Button } from '../buttonv2/Buttonv2.component';
8
+ import { Icon } from '../icon/Icon.component';
9
+ import { Text } from '../text/Text.component';
10
+
11
+ type DrawerPosition = 'left' | 'right' | 'top' | 'bottom';
12
+
13
+ type Props = {
14
+ isOpen: boolean;
15
+ close: () => void;
16
+ title: ReactNode;
17
+ position?: DrawerPosition;
18
+ size?: CSSProperties['width'];
19
+ footer?: ReactNode;
20
+ overlay?: boolean;
21
+ showCloseButton?: boolean;
22
+ children: ReactNode;
23
+ };
24
+
25
+ const TRANSITION_DURATION = '250ms';
26
+
27
+ const DrawerBackdrop = styled.div<{ $overlay: boolean }>`
28
+ position: fixed;
29
+ inset: 0;
30
+ z-index: ${zIndex.overlay};
31
+ background: ${({ $overlay }) => ($overlay ? 'rgba(0, 0, 0, 0.3)' : 'transparent')};
32
+ `;
33
+
34
+ function getTransform(position: DrawerPosition, isOpen: boolean): string {
35
+ if (isOpen) return 'translate3d(0, 0, 0)';
36
+ switch (position) {
37
+ case 'left':
38
+ return 'translate3d(-100%, 0, 0)';
39
+ case 'right':
40
+ return 'translate3d(100%, 0, 0)';
41
+ case 'top':
42
+ return 'translate3d(0, -100%, 0)';
43
+ case 'bottom':
44
+ return 'translate3d(0, 100%, 0)';
45
+ }
46
+ }
47
+
48
+ const DrawerPanel = styled.div<{
49
+ $position: DrawerPosition;
50
+ $size: CSSProperties['width'];
51
+ $isOpen: boolean;
52
+ }>`
53
+ position: fixed;
54
+ z-index: ${zIndex.drawer};
55
+ background-color: ${getThemePropSelector('backgroundLevel2')};
56
+ color: ${getThemePropSelector('textPrimary')};
57
+ display: flex;
58
+ flex-direction: column;
59
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.3);
60
+ transition: transform ${TRANSITION_DURATION} ease-in-out;
61
+ transform: ${({ $position, $isOpen }) => getTransform($position, $isOpen)};
62
+
63
+ ${({ $position, $size, theme }) => {
64
+ const borderSide = $position === 'left' ? 'right' : $position === 'right' ? 'left' : $position === 'top' ? 'bottom' : 'top';
65
+ const border = `border-${borderSide}: 1px solid ${theme.border};`;
66
+ switch ($position) {
67
+ case 'left':
68
+ return `top: 0; left: 0; bottom: 0; width: ${$size}; ${border}`;
69
+ case 'right':
70
+ return `top: 0; right: 0; bottom: 0; width: ${$size}; ${border}`;
71
+ case 'top':
72
+ return `top: 0; left: 0; right: 0; height: ${$size}; ${border}`;
73
+ case 'bottom':
74
+ return `bottom: 0; left: 0; right: 0; height: ${$size}; ${border}`;
75
+ }
76
+ }}
77
+ `;
78
+
79
+ const DrawerHeader = styled.div`
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: space-between;
83
+ padding: ${spacing.r16};
84
+ background-color: ${getThemePropSelector('backgroundLevel3')};
85
+ `;
86
+
87
+ const DrawerBody = styled.div`
88
+ flex: 1;
89
+ overflow-y: auto;
90
+ padding: ${spacing.r16};
91
+ background-color: ${getThemePropSelector('backgroundLevel4')};
92
+ `;
93
+
94
+ const DrawerFooter = styled.div`
95
+ padding: ${spacing.r16};
96
+ background-color: ${getThemePropSelector('backgroundLevel3')};
97
+ display: flex;
98
+ gap: ${spacing.r8};
99
+ justify-content: flex-end;
100
+ `;
101
+
102
+ const Drawer = ({
103
+ isOpen,
104
+ close,
105
+ title,
106
+ position = 'left',
107
+ size = '400px',
108
+ footer,
109
+ overlay = false,
110
+ showCloseButton = true,
111
+ children,
112
+ }: Props) => {
113
+ const drawerContainer = useRef(document.createElement('div'));
114
+ const panelRef = useRef<HTMLDivElement>(null);
115
+ const [mounted, setMounted] = useState(isOpen);
116
+ const [active, setActive] = useState(false);
117
+
118
+ useLayoutEffect(() => {
119
+ const container = drawerContainer.current;
120
+ document.body?.prepend(container);
121
+ return () => {
122
+ document.body?.removeChild(container);
123
+ };
124
+ }, []);
125
+
126
+ useEffect(() => {
127
+ if (isOpen) {
128
+ setMounted(true);
129
+ } else {
130
+ setActive(false);
131
+ }
132
+ }, [isOpen]);
133
+
134
+ useEffect(() => {
135
+ if (mounted && isOpen) {
136
+ const frame = requestAnimationFrame(() => {
137
+ setActive(true);
138
+ panelRef.current?.focus();
139
+ });
140
+ return () => cancelAnimationFrame(frame);
141
+ }
142
+ }, [mounted, isOpen]);
143
+
144
+ const stableClose = useCallback(close, [close]);
145
+
146
+ useEffect(() => {
147
+ if (isOpen) {
148
+ const handleEsc = (event: KeyboardEvent) => {
149
+ if (event.key === 'Escape') {
150
+ stableClose();
151
+ }
152
+ };
153
+ document.addEventListener('keydown', handleEsc);
154
+ return () => {
155
+ document.removeEventListener('keydown', handleEsc);
156
+ };
157
+ }
158
+ }, [isOpen, stableClose]);
159
+
160
+ const handleTransitionEnd = () => {
161
+ if (!isOpen) setMounted(false);
162
+ };
163
+
164
+ if (!mounted) return null;
165
+
166
+ return createPortal(
167
+ <>
168
+ <DrawerBackdrop $overlay={overlay} onClick={stableClose} />
169
+ <DrawerPanel
170
+ ref={panelRef}
171
+ className="sc-drawer"
172
+ $position={position}
173
+ $size={size}
174
+ $isOpen={active}
175
+ role="dialog"
176
+ tabIndex={-1}
177
+ aria-modal={overlay ? 'true' : undefined}
178
+ aria-labelledby="drawer_label"
179
+ onTransitionEnd={handleTransitionEnd}
180
+ >
181
+ <DrawerHeader className="sc-drawer-header">
182
+ <Text variant="Larger" id="drawer_label">
183
+ {title}
184
+ </Text>
185
+ {showCloseButton && (
186
+ <Button
187
+ icon={<Icon name="Close" />}
188
+ onClick={stableClose}
189
+ tooltip={{ overlay: 'Close' }}
190
+ aria-label="Close"
191
+ />
192
+ )}
193
+ </DrawerHeader>
194
+ <DrawerBody className="sc-drawer-body">{children}</DrawerBody>
195
+ {footer && (
196
+ <DrawerFooter className="sc-drawer-footer">
197
+ {footer}
198
+ </DrawerFooter>
199
+ )}
200
+ </DrawerPanel>
201
+ </>,
202
+ drawerContainer.current,
203
+ );
204
+ };
205
+
206
+ export { Drawer };
207
+ export type { DrawerPosition };
package/src/lib/index.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  } from './components/constants';
15
15
  export type { Status } from './components/constants';
16
16
  export { Layout } from './components/layout/Layout.component';
17
+ export { Drawer } from './components/drawer/Drawer.component';
17
18
  export { Loader } from './components/loader/Loader.component';
18
19
  export { Modal } from './components/modal/Modal.component';
19
20
  export { Navbar } from './components/navbar/Navbar.component';
package/src/lib/next.ts CHANGED
@@ -17,6 +17,7 @@ export { CoreUiThemeProvider } from './components/coreuithemeprovider/CoreUiThem
17
17
  export { Box } from './components/box/Box';
18
18
  export { Input } from './components/inputv2/inputv2';
19
19
  export { Accordion } from './components/accordion/Accordion.component';
20
+ export { Drawer } from './components/drawer/Drawer.component';
20
21
  export { Editor } from './components/editor';
21
22
  export type { EditorProps } from './components/editor';
22
23
 
@@ -310,6 +310,7 @@ export const zIndex = {
310
310
  tooltip: 9990,
311
311
  notification: 9000,
312
312
  modal: 8500,
313
+ drawer: 8200,
313
314
  overlay: 8000,
314
315
  dropdown: 7000,
315
316
  nav: 500,
@@ -4,22 +4,28 @@ const LIGHT_TEXT = '#EAEAEA';
4
4
  const DARK_TEXT = '#000000';
5
5
 
6
6
  describe('getContrastText', () => {
7
- it('returns textPrimary on dark backgrounds when textPrimary is light', () => {
7
+ it('returns light text on dark backgrounds', () => {
8
8
  expect(getContrastText('#000000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
9
9
  expect(getContrastText('#1A1A1A', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
10
10
  expect(getContrastText('#121219', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
11
11
  expect(getContrastText('#2F4185', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
12
12
  });
13
13
 
14
- it('returns textReverse on light backgrounds when textPrimary is light', () => {
14
+ it('returns dark text on light backgrounds', () => {
15
15
  expect(getContrastText('#FFFFFF', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
16
16
  expect(getContrastText('#F5F5F5', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
17
17
  expect(getContrastText('#FCFCFC', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
18
18
  });
19
19
 
20
- it('picks the text color with better contrast against a vivid background', () => {
21
- expect(getContrastText('#E9041E', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
22
- expect(getContrastText('#E9041E', '#FFFFFF', '#000000')).toBe('#FFFFFF');
20
+ it('returns light text on saturated colors where WCAG luminance is ambiguous', () => {
21
+ expect(getContrastText('#E60028', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
22
+ expect(getContrastText('#E9041E', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
23
+ expect(getContrastText('#E60028', '#0D0D0D', '#EAEAEA')).toBe('#EAEAEA');
24
+ });
25
+
26
+ it('works regardless of which token is lighter', () => {
27
+ expect(getContrastText('#000000', DARK_TEXT, LIGHT_TEXT)).toBe(LIGHT_TEXT);
28
+ expect(getContrastText('#FFFFFF', DARK_TEXT, LIGHT_TEXT)).toBe(DARK_TEXT);
23
29
  });
24
30
 
25
31
  it('handles 3-character hex shorthand', () => {
@@ -27,12 +33,12 @@ describe('getContrastText', () => {
27
33
  expect(getContrastText('#000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
28
34
  });
29
35
 
30
- it('handles hex without # prefix', () => {
31
- expect(getContrastText('000000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
32
- expect(getContrastText('FFFFFF', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
36
+ it('handles rgb color format', () => {
37
+ expect(getContrastText('rgb(0, 0, 0)', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
38
+ expect(getContrastText('rgb(255, 255, 255)', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
33
39
  });
34
40
 
35
- it('returns null for non-hex values', () => {
41
+ it('returns null for unparseable values', () => {
36
42
  expect(
37
43
  getContrastText(
38
44
  'linear-gradient(130deg, #9355E7 0%, #2E4AA3 60%)',
@@ -41,8 +47,5 @@ describe('getContrastText', () => {
41
47
  ),
42
48
  ).toBeNull();
43
49
  expect(getContrastText('not-a-color', LIGHT_TEXT, DARK_TEXT)).toBeNull();
44
- expect(
45
- getContrastText('rgb(255, 0, 0)', LIGHT_TEXT, DARK_TEXT),
46
- ).toBeNull();
47
50
  });
48
51
  });
package/src/lib/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { getLuminance } from 'polished';
2
+
1
3
  const RGB_HEX = /^#?(?:([\da-f]{3})[\da-f]?|([\da-f]{6})(?:[\da-f]{2})?)$/i;
2
4
 
3
5
  /** Ensure the consistency of colors between old and new colors */
@@ -45,22 +47,12 @@ export const hex2RGB = (str: string): [number, number, number] => {
45
47
  throw new Error('Invalid hex string provided');
46
48
  };
47
49
 
48
- // WCAG 2.0 relative luminance
49
- const relativeLuminance = (r: number, g: number, b: number): number => {
50
- const [rs, gs, bs] = [r, g, b].map((c) => {
51
- const s = c / 255;
52
- return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
53
- });
54
- return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
55
- };
56
-
57
50
  const wcagContrastRatio = (l1: number, l2: number): number =>
58
51
  (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
59
52
 
60
- const luminanceOf = (hex: string): number => {
61
- const [r, g, b] = hex2RGB(hex);
62
- return relativeLuminance(r, g, b);
63
- };
53
+ // Minimum WCAG contrast ratio to consider a text color readable on a background.
54
+ // 3.0 corresponds to WCAG AA for large text — same threshold used by MUI.
55
+ const CONTRAST_THRESHOLD = 3;
64
56
 
65
57
  export const getContrastText = (
66
58
  bgColor: string,
@@ -68,10 +60,16 @@ export const getContrastText = (
68
60
  textReverse: string,
69
61
  ): string | null => {
70
62
  try {
71
- const bgLum = luminanceOf(bgColor);
72
- const primaryContrast = wcagContrastRatio(luminanceOf(textPrimary), bgLum);
73
- const reverseContrast = wcagContrastRatio(luminanceOf(textReverse), bgLum);
74
- return reverseContrast > primaryContrast ? textReverse : textPrimary;
63
+ const bgLum = getLuminance(bgColor);
64
+ const primaryLum = getLuminance(textPrimary);
65
+ const reverseLum = getLuminance(textReverse);
66
+
67
+ const lighterText = primaryLum >= reverseLum ? textPrimary : textReverse;
68
+ const darkerText = primaryLum >= reverseLum ? textReverse : textPrimary;
69
+
70
+ const lighterContrast = wcagContrastRatio(primaryLum >= reverseLum ? primaryLum : reverseLum, bgLum);
71
+
72
+ return lighterContrast >= CONTRAST_THRESHOLD ? lighterText : darkerText;
75
73
  } catch {
76
74
  return null;
77
75
  }
@@ -0,0 +1,50 @@
1
+ # eslint-plugin-valalint
2
+
3
+ A custom ESLint plugin shipped as part of `@scality/core-ui`.
4
+ It enforces Scality UI coding conventions (e.g. capitalisation rules for user-facing text).
5
+
6
+ ## Using the plugin in a project
7
+
8
+ Install (or depend on) `@scality/core-ui`, then import the plugin from its sub-path:
9
+
10
+ ### Example config (`eslint.config.mjs`)
11
+
12
+ ```js
13
+ import valalint from '@scality/core-ui/eslint-plugin';
14
+
15
+ export default [
16
+ {
17
+ files: ['**/*.{js,jsx,ts,tsx}'],
18
+ ignores: ['src/gen/**'],
19
+ ...valalint.configs['flat/recommended'],
20
+ },
21
+ ];
22
+ ```
23
+
24
+ ## Adding a new rule
25
+
26
+ ### 1. Create the rule file
27
+
28
+ Add a new file under `src/lib/valalint/rules/`. Use the existing
29
+ [`technical-sentence-case.js`](./rules/technical-sentence-case.js) as a reference.
30
+
31
+ Refer to the [ESLint rule authoring docs](https://eslint.org/docs/latest/extend/custom-rules) for the full API.
32
+
33
+ ### 2. Register the rule in the plugin
34
+
35
+ Open [`src/lib/valalint/index.js`](./index.js) and add two lines to import your rule and add it to the rules map.
36
+
37
+ ### 3. Set a default severity in the recommended config
38
+
39
+ Still in `index.js`, add the rule to `recommendedRules` with its desired default severity
40
+ (`'off'`, `'warn'`, or `'error'`):
41
+
42
+ ```js
43
+ const recommendedRules = {
44
+ 'valalint/technical-sentence-case': 'warn',
45
+ 'valalint/my-new-rule': 'warn', // ← add this
46
+ };
47
+ ```
48
+
49
+ That's it. The rule is now available to all consumers via both the `recommended` and
50
+ `flat/recommended` configs, and individually as `'valalint/my-new-rule'`.
@@ -0,0 +1,49 @@
1
+ import tsParser from '@typescript-eslint/parser';
2
+ import technicalSentenceCase from './rules/technical-sentence-case.js';
3
+ import modalButtonForbiddenLabel from './rules/modal-button-forbidden-label.js';
4
+ import noRawNumberInJsx from './rules/no-raw-number-in-jsx.js';
5
+
6
+ const rules = {
7
+ 'technical-sentence-case': technicalSentenceCase,
8
+ 'modal-button-forbidden-label': modalButtonForbiddenLabel,
9
+ 'no-raw-number-in-jsx': noRawNumberInJsx,
10
+ };
11
+
12
+ /** Default rule severity for the recommended config. */
13
+ const recommendedRules = {
14
+ 'valalint/technical-sentence-case': 'warn',
15
+ 'valalint/modal-button-forbidden-label': 'warn',
16
+ 'valalint/no-raw-number-in-jsx': 'warn',
17
+ };
18
+
19
+ const plugin = {
20
+ meta: {
21
+ name: 'valalint',
22
+ version: '1.0.0',
23
+ },
24
+
25
+ rules,
26
+ configs: {
27
+ /** Legacy eslintrc-style recommended config. */
28
+ recommended: {
29
+ plugins: ['valalint'],
30
+ rules: recommendedRules,
31
+ },
32
+ },
33
+ };
34
+
35
+ plugin.configs['flat/recommended'] = {
36
+ plugins: { valalint: plugin },
37
+ rules: recommendedRules,
38
+ languageOptions: {
39
+ parser: tsParser,
40
+ parserOptions: {
41
+ ecmaFeatures: {
42
+ jsx: true,
43
+ },
44
+ project: true,
45
+ },
46
+ },
47
+ };
48
+
49
+ export default plugin;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Matches whole words "yes", "no", or "ok" case-insensitively.
3
+ * "Okay", "Book", "Note", "Yesterday" etc. are intentionally NOT matched.
4
+ */
5
+ const FORBIDDEN_PATTERN = /\b(yes|no|ok)\b/i;
6
+
7
+ /**
8
+ * Walks up the ancestor chain to determine whether `node` is a descendant
9
+ * of a <Modal> JSX element (including elements nested inside JSX attribute
10
+ * expression containers such as the `footer` prop).
11
+ */
12
+ function isInsideModal(node) {
13
+ let current = node.parent;
14
+ while (current) {
15
+ if (
16
+ current.type === 'JSXElement' &&
17
+ current.openingElement?.name?.name === 'Modal'
18
+ ) {
19
+ return true;
20
+ }
21
+ current = current.parent;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ const modalButtonForbiddenLabel = {
27
+ meta: {
28
+ type: 'suggestion',
29
+ docs: {
30
+ description:
31
+ 'Disallow Button labels containing "Yes", "No", or "Ok" inside a Modal',
32
+ category: 'Best Practices',
33
+ recommended: false,
34
+ },
35
+ schema: [],
36
+ },
37
+
38
+ create(context) {
39
+ return {
40
+ JSXElement(node) {
41
+ if (node.openingElement.name.name !== 'Button') return;
42
+ if (!isInsideModal(node)) return;
43
+
44
+ // ── Check JSX text children ─────────────────────────────────────────
45
+ node.children.forEach(child => {
46
+ if (child.type !== 'JSXText') return;
47
+ const text = child.value.trim();
48
+ if (text && FORBIDDEN_PATTERN.test(text)) {
49
+ context.report({
50
+ node: child,
51
+ message:
52
+ 'Button inside Modal should not use "Yes", "No", or "Ok" as a label.',
53
+ });
54
+ }
55
+ });
56
+
57
+ // ── Check label prop ────────────────────────────────────────────────
58
+ const labelProp = node.openingElement.attributes.find(
59
+ attr => attr.type === 'JSXAttribute' && attr.name.name === 'label',
60
+ );
61
+ if (!labelProp?.value) return;
62
+
63
+ let labelText = null;
64
+ if (labelProp.value.type === 'Literal') {
65
+ // label="Ok, delete"
66
+ labelText = String(labelProp.value.value);
67
+ } else if (
68
+ labelProp.value.type === 'JSXExpressionContainer' &&
69
+ labelProp.value.expression.type === 'Literal'
70
+ ) {
71
+ // label={"Ok, delete"}
72
+ labelText = String(labelProp.value.expression.value);
73
+ }
74
+
75
+ if (labelText && FORBIDDEN_PATTERN.test(labelText)) {
76
+ context.report({
77
+ node: labelProp,
78
+ message:
79
+ 'Button inside Modal should not use "Yes", "No", or "Ok" as a label.',
80
+ });
81
+ }
82
+ },
83
+ };
84
+ },
85
+ };
86
+
87
+ export default modalButtonForbiddenLabel;