@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,157 @@
1
+ import { RuleTester } from 'eslint';
2
+ import rule from './modal-button-forbidden-label.js';
3
+ import * as tsParser from '@typescript-eslint/parser';
4
+
5
+ const tester = new RuleTester({
6
+ parser: tsParser,
7
+ parserOptions: {
8
+ ecmaFeatures: { jsx: true },
9
+ ecmaVersion: 2020,
10
+ },
11
+ });
12
+
13
+ const ERROR = 'Button inside Modal should not use "Yes", "No", or "Ok" as a label.';
14
+
15
+ tester.run('modal-button-forbidden-label', rule, {
16
+ // ─── Valid ────────────────────────────────────────────────────────────────
17
+ valid: [
18
+ // Buttons outside a Modal are never checked, even if the label is forbidden
19
+ { code: '<Button>Yes</Button>' },
20
+ { code: '<Button>No</Button>' },
21
+ { code: '<Button>Ok</Button>' },
22
+ { code: '<Button label="Ok" />' },
23
+
24
+ // Non-Modal wrappers do not trigger the rule
25
+ { code: '<Dialog><Button>Yes</Button></Dialog>' },
26
+
27
+ // Buttons inside Modal with acceptable labels
28
+ { code: '<Modal><Button>Cancel</Button></Modal>' },
29
+ { code: '<Modal><Button>Confirm</Button></Modal>' },
30
+ { code: '<Modal><Button>Delete item</Button></Modal>' },
31
+ { code: '<Modal><Button label="Cancel" /></Modal>' },
32
+ { code: '<Modal><Button label="Delete item" /></Modal>' },
33
+ { code: '<Modal footer={<Button label="Cancel" />}>Content</Modal>' },
34
+
35
+ // Non-Button elements inside Modal are not checked
36
+ { code: '<Modal><Text>Yes</Text></Modal>' },
37
+ { code: '<Modal><span>No</span></Modal>' },
38
+
39
+ // Words that merely contain the forbidden substring are NOT matched
40
+ // ("Okay" → "ok" is not a whole word because "a" follows it)
41
+ { code: '<Modal><Button>Okay</Button></Modal>' },
42
+ { code: '<Modal><Button label="Rebook" /></Modal>' },
43
+ { code: '<Modal><Button label="Nobody" /></Modal>' },
44
+
45
+ // Empty / whitespace-only children are ignored
46
+ { code: '<Modal><Button> </Button></Modal>' },
47
+ { code: '<Modal><Button>{variable}</Button></Modal>' },
48
+ ],
49
+
50
+ // ─── Invalid ──────────────────────────────────────────────────────────────
51
+ invalid: [
52
+ // ── JSX text children ─────────────────────────────────────────────────
53
+
54
+ {
55
+ code: '<Modal><Button>Yes</Button></Modal>',
56
+ errors: [{ message: ERROR }],
57
+ },
58
+ {
59
+ code: '<Modal><Button>No</Button></Modal>',
60
+ errors: [{ message: ERROR }],
61
+ },
62
+ {
63
+ code: '<Modal><Button>Ok</Button></Modal>',
64
+ errors: [{ message: ERROR }],
65
+ },
66
+ {
67
+ // case-insensitive matching
68
+ code: '<Modal><Button>yes</Button></Modal>',
69
+ errors: [{ message: ERROR }],
70
+ },
71
+ {
72
+ code: '<Modal><Button>no</Button></Modal>',
73
+ errors: [{ message: ERROR }],
74
+ },
75
+ {
76
+ code: '<Modal><Button>ok</Button></Modal>',
77
+ errors: [{ message: ERROR }],
78
+ },
79
+ {
80
+ // forbidden word embedded in a longer label
81
+ code: '<Modal><Button>Ok, got it</Button></Modal>',
82
+ errors: [{ message: ERROR }],
83
+ },
84
+
85
+ // ── label prop — string literal ────────────────────────────────────────
86
+
87
+ {
88
+ code: '<Modal><Button label="Yes" /></Modal>',
89
+ errors: [{ message: ERROR }],
90
+ },
91
+ {
92
+ code: '<Modal><Button label="No" /></Modal>',
93
+ errors: [{ message: ERROR }],
94
+ },
95
+ {
96
+ code: '<Modal><Button label="Ok" /></Modal>',
97
+ errors: [{ message: ERROR }],
98
+ },
99
+ {
100
+ // real-world example from the rule description
101
+ code: '<Modal><Button label="Ok, delete" /></Modal>',
102
+ errors: [{ message: ERROR }],
103
+ },
104
+
105
+ // ── label prop — JSX expression container ─────────────────────────────
106
+
107
+ {
108
+ code: '<Modal><Button label={"Yes"} /></Modal>',
109
+ errors: [{ message: ERROR }],
110
+ },
111
+
112
+ // ── Button nested inside a JSX attribute expression (e.g. footer) ─────
113
+
114
+ {
115
+ code: '<Modal footer={<Button label="Ok, delete" />}>Content</Modal>',
116
+ errors: [{ message: ERROR }],
117
+ },
118
+ {
119
+ code: '<Modal footer={<Button label="No" />}>Content</Modal>',
120
+ errors: [{ message: ERROR }],
121
+ },
122
+ {
123
+ // Button nested deeper inside the footer expression
124
+ code: '<Modal footer={<Box><Button label="Yes" /></Box>}>Content</Modal>',
125
+ errors: [{ message: ERROR }],
126
+ },
127
+
128
+ // ── All three targeted components (Button) are covered ────────────────
129
+
130
+ {
131
+ // Mirrors the example from the rule description
132
+ code: `
133
+ <Modal
134
+ isOpen={isModalOpen}
135
+ title="Remove deployment"
136
+ footer={
137
+ <Box>
138
+ <Button variant="secondary" label="Cancel" />
139
+ <Button variant="danger" label="Ok, delete" />
140
+ </Box>
141
+ }
142
+ >
143
+ Are you sure?
144
+ </Modal>`,
145
+ errors: [{ message: ERROR }],
146
+ },
147
+ ],
148
+ });
149
+
150
+ // RuleTester.run() throws if any case fails, so reaching this line means all
151
+ // cases passed. Jest needs at least one explicit assertion per test file.
152
+ describe('modal-button-forbidden-label', () => {
153
+ it('passes all RuleTester cases', () => {
154
+ // Execution of tester.run() above already validated everything.
155
+ expect(true).toBe(true);
156
+ });
157
+ });
@@ -0,0 +1,64 @@
1
+ import ts from 'typescript';
2
+
3
+ /**
4
+ * Returns true when the TypeScript type — or any member of a union — is a
5
+ * numeric type (number primitive or a number literal type such as `42`).
6
+ */
7
+ function typeIncludesNumber(type) {
8
+ if (type.flags & ts.TypeFlags.Number) return true;
9
+ if (type.flags & ts.TypeFlags.NumberLiteral) return true;
10
+ // Handle union types: `number | undefined`, `number | null`, `string | number`, …
11
+ if (type.flags & ts.TypeFlags.Union) {
12
+ return type.types.some(t => typeIncludesNumber(t));
13
+ }
14
+ return false;
15
+ }
16
+
17
+ const noRawNumberInJsx = {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description:
22
+ 'Disallow raw numeric expressions in JSX text; use formatISONumber() instead',
23
+ category: 'Best Practices',
24
+ recommended: false,
25
+ },
26
+ schema: [],
27
+ messages: {
28
+ rawNumber:
29
+ 'Avoid rendering raw numbers in JSX text. ' +
30
+ 'Use formatISONumber(value) to apply proper ISO formatting ' +
31
+ '(space as thousands separator, leading zero before decimal point).',
32
+ },
33
+ },
34
+
35
+ create(context) {
36
+ // Type-aware rule: opt out gracefully when no TypeScript program is available.
37
+ const services = context.sourceCode?.parserServices ?? context.parserServices;
38
+ if (!services?.program) return {};
39
+
40
+ const checker = services.program.getTypeChecker();
41
+
42
+ return {
43
+ JSXExpressionContainer(node) {
44
+ // Only flag children positions — attribute values are intentionally excluded.
45
+ // A child container's direct parent is JSXElement; an attribute value's is JSXAttribute.
46
+ if (node.parent.type !== 'JSXElement') return;
47
+
48
+ const { expression } = node;
49
+
50
+ // Skip {/* JSX comments */}
51
+ if (expression.type === 'JSXEmptyExpression') return;
52
+
53
+ const tsNode = services.esTreeNodeToTSNodeMap.get(expression);
54
+ const type = checker.getTypeAtLocation(tsNode);
55
+
56
+ if (typeIncludesNumber(type)) {
57
+ context.report({ node, messageId: 'rawNumber' });
58
+ }
59
+ },
60
+ };
61
+ },
62
+ };
63
+
64
+ export default noRawNumberInJsx;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+ import { RuleTester } from '@typescript-eslint/rule-tester';
5
+ import rule from './no-raw-number-in-jsx.js';
6
+ import path from 'path';
7
+
8
+ // Jest runs from the project root, so process.cwd() is the workspace root
9
+ // where tsconfig.json lives.
10
+ const tsconfigRootDir = process.cwd();
11
+
12
+ // Virtual filename placed at the workspace root so it is matched by the
13
+ // allowDefaultProject glob ('*.tsx') and uses the workspace tsconfig.
14
+ const filename = path.join(tsconfigRootDir, 'test.tsx');
15
+
16
+ const tester = new RuleTester({
17
+ languageOptions: {
18
+ parserOptions: {
19
+ ecmaFeatures: { jsx: true },
20
+ ecmaVersion: 2020,
21
+ projectService: {
22
+ allowDefaultProject: ['*.tsx'],
23
+ defaultProject: 'tsconfig.json',
24
+ },
25
+ tsconfigRootDir,
26
+ },
27
+ },
28
+ });
29
+
30
+ const ERROR =
31
+ 'Avoid rendering raw numbers in JSX text. Use formatISONumber(value) to apply proper ISO formatting (space as thousands separator, leading zero before decimal point).';
32
+
33
+ tester.run('no-raw-number-in-jsx', rule, {
34
+ // ─── Valid ────────────────────────────────────────────────────────────────
35
+ valid: [
36
+ // ── Numbers in JSX attribute values — never flagged ───────────────────
37
+ {
38
+ // Numeric literal in an attribute
39
+ code: 'const el = <input value={42} />;',
40
+ filename,
41
+ },
42
+ {
43
+ // Typed number variable in an attribute
44
+ code: 'declare const max: number; const el = <Chart max={max} />;',
45
+ filename,
46
+ },
47
+ {
48
+ // Multiple numeric attributes
49
+ code: 'declare const n: number; const el = <Slider step={0.5} min={0} max={n} />;',
50
+ filename,
51
+ },
52
+
53
+ // ── String types in JSX children — not flagged ────────────────────────
54
+ {
55
+ code: 'declare const name: string; const el = <Text>{name}</Text>;',
56
+ filename,
57
+ },
58
+ {
59
+ code: 'const el = <Text>{"hello"}</Text>;',
60
+ filename,
61
+ },
62
+ {
63
+ code: 'const el = <Text>{`template`}</Text>;',
64
+ filename,
65
+ },
66
+ {
67
+ // Template literal containing a number expression is still a string
68
+ // eslint-disable-next-line no-template-curly-in-string
69
+ code: 'declare const count: number; const el = <Text>{`Total: ${count}`}</Text>;',
70
+ filename,
71
+ },
72
+
73
+ // ── formatISONumber returns string — not flagged ───────────────────────
74
+ {
75
+ code: 'declare function formatISONumber(n: number, opts?: object): string; declare const count: number; const el = <Text>{formatISONumber(count)}</Text>;',
76
+ filename,
77
+ },
78
+
79
+ // ── Methods that return strings — not flagged ──────────────────────────
80
+ {
81
+ // Number.prototype.toFixed returns string
82
+ code: 'declare const value: number; const el = <Text>{value.toFixed(2)}</Text>;',
83
+ filename,
84
+ },
85
+ {
86
+ // Number.prototype.toPrecision returns string
87
+ code: 'declare const value: number; const el = <Text>{value.toPrecision(4)}</Text>;',
88
+ filename,
89
+ },
90
+ {
91
+ // String.prototype.toUpperCase returns string
92
+ code: 'declare const label: string; const el = <Text>{label.toUpperCase()}</Text>;',
93
+ filename,
94
+ },
95
+
96
+ // ── Other non-numeric types — not flagged ─────────────────────────────
97
+ {
98
+ code: 'const el = <Text>{null}</Text>;',
99
+ filename,
100
+ },
101
+ {
102
+ code: 'declare const flag: boolean; const el = <Text>{flag}</Text>;',
103
+ filename,
104
+ },
105
+ {
106
+ // string + number → string (TS widens to string)
107
+ code: 'declare const count: number; const el = <Text>{count + " items"}</Text>;',
108
+ filename,
109
+ },
110
+ {
111
+ // Conditional producing string
112
+ code: 'declare const flag: boolean; const el = <Text>{flag ? "yes" : "no"}</Text>;',
113
+ filename,
114
+ },
115
+
116
+ // ── JSX comment — not flagged ──────────────────────────────────────────
117
+ {
118
+ code: 'const el = <Text>{/* comment */}</Text>;',
119
+ filename,
120
+ },
121
+ ],
122
+
123
+ // ─── Invalid ──────────────────────────────────────────────────────────────
124
+ invalid: [
125
+ // ── Numeric literals (TypeFlags.NumberLiteral) ─────────────────────────
126
+ {
127
+ code: 'const el = <Text>{42}</Text>;',
128
+ filename,
129
+ errors: [{ message: ERROR }],
130
+ },
131
+ {
132
+ code: 'const el = <Text>{0}</Text>;',
133
+ filename,
134
+ errors: [{ message: ERROR }],
135
+ },
136
+ {
137
+ code: 'const el = <Text>{3.14}</Text>;',
138
+ filename,
139
+ errors: [{ message: ERROR }],
140
+ },
141
+
142
+ // ── Typed number variables (TypeFlags.Number) ─────────────────────────
143
+ {
144
+ code: 'declare const count: number; const el = <Text>{count}</Text>;',
145
+ filename,
146
+ errors: [{ message: ERROR }],
147
+ },
148
+ {
149
+ // Works on any element tag
150
+ code: 'declare const price: number; const el = <Tooltip>{price}</Tooltip>;',
151
+ filename,
152
+ errors: [{ message: ERROR }],
153
+ },
154
+
155
+ // ── Arithmetic that produces a number ─────────────────────────────────
156
+ {
157
+ // number + number → number
158
+ code: 'declare const a: number; declare const b: number; const el = <Text>{a + b}</Text>;',
159
+ filename,
160
+ errors: [{ message: ERROR }],
161
+ },
162
+ {
163
+ code: 'declare const total: number; declare const tax: number; const el = <Text>{total - tax}</Text>;',
164
+ filename,
165
+ errors: [{ message: ERROR }],
166
+ },
167
+ {
168
+ code: 'declare const bytes: number; const el = <Text>{bytes / 1024}</Text>;',
169
+ filename,
170
+ errors: [{ message: ERROR }],
171
+ },
172
+
173
+ // ── Unary minus on a number ───────────────────────────────────────────
174
+ {
175
+ code: 'declare const n: number; const el = <Text>{-n}</Text>;',
176
+ filename,
177
+ errors: [{ message: ERROR }],
178
+ },
179
+
180
+ // ── Properties that return number ─────────────────────────────────────
181
+ {
182
+ // Array.length → number
183
+ code: 'declare const items: string[]; const el = <Text>{items.length}</Text>;',
184
+ filename,
185
+ errors: [{ message: ERROR }],
186
+ },
187
+
188
+ // ── Functions that return number ───────────────────────────────────────
189
+ {
190
+ code: 'declare function getCount(): number; const el = <Text>{getCount()}</Text>;',
191
+ filename,
192
+ errors: [{ message: ERROR }],
193
+ },
194
+
195
+ // ── Union types that include number ───────────────────────────────────
196
+ {
197
+ // number | undefined — when defined it would render an unformatted number
198
+ code: 'declare const count: number | undefined; const el = <Text>{count}</Text>;',
199
+ filename,
200
+ errors: [{ message: ERROR }],
201
+ },
202
+ {
203
+ // string | number
204
+ code: 'declare const value: string | number; const el = <Text>{value}</Text>;',
205
+ filename,
206
+ errors: [{ message: ERROR }],
207
+ },
208
+
209
+ // ── Real-world patterns ────────────────────────────────────────────────
210
+ {
211
+ // Numeric literal mixed with JSX text siblings
212
+ code: 'const el = <Text>Total: {1234567}</Text>;',
213
+ filename,
214
+ errors: [{ message: ERROR }],
215
+ },
216
+ {
217
+ // Multiple violations in the same element
218
+ code: 'declare const a: number; declare const b: number; const el = <Text>{a}{b}</Text>;',
219
+ filename,
220
+ errors: [{ message: ERROR }, { message: ERROR }],
221
+ },
222
+ {
223
+ // Nested expressions
224
+ code: 'declare const a: number; declare const b: number; const el = <Text><span>{a}</span></Text>;',
225
+ filename,
226
+ errors: [{ message: ERROR }],
227
+ },
228
+ ],
229
+ });
230
+
231
+ // RuleTester.run() throws if any case fails, so reaching this line means all
232
+ // cases passed. Jest needs at least one explicit assertion per test file.
233
+ describe('no-raw-number-in-jsx', () => {
234
+ it('passes all RuleTester cases', () => {
235
+ expect(true).toBe(true);
236
+ });
237
+ });
@@ -0,0 +1,93 @@
1
+ const RESOURCE_NAMES = [
2
+ 'Bucket', 'Object', 'Node', 'Disk', 'Volume', 'Cluster', 'Policy', 'User',
3
+ 'Group', 'Role', 'Workflow', 'Rule', 'Account', 'License', 'Location', 'Alert', 'Certificate'
4
+ ];
5
+ const TARGET_COMPONENTS = ['Button', 'Text', 'Tooltip'];
6
+
7
+ function checkSentenceCase(text) {
8
+ const trimmed = text.trim();
9
+ if (!trimmed) return true;
10
+ const firstLetter = trimmed.charAt(0);
11
+ return firstLetter === firstLetter.toUpperCase();
12
+ }
13
+
14
+ function checkResourceNames(text) {
15
+ for (const resource of RESOURCE_NAMES) {
16
+ const regex = new RegExp(`\\b${resource.toLowerCase()}\\b`);
17
+ const match = text.match(regex);
18
+ if (match) {
19
+ return resource;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ const technicalSentenceCase = {
26
+ meta: {
27
+ type: 'suggestion',
28
+ docs: {
29
+ description: 'Enforce Technical Sentence Case for UI text',
30
+ category: 'Stylistic Issues',
31
+ recommended: false,
32
+ },
33
+ schema: [],
34
+ fixable: 'code',
35
+ },
36
+
37
+ create(context) {
38
+ return {
39
+ JSXElement(node) {
40
+ const name = node.openingElement.name.name;
41
+ if (!TARGET_COMPONENTS.includes(name)) return;
42
+
43
+ // Find the index of the first non-empty JSXText child that appears
44
+ // before any non-text node. Sentence case only applies to that child.
45
+ let firstTextChildIndex = -1;
46
+ for (let i = 0; i < node.children.length; i++) {
47
+ const child = node.children[i];
48
+ if (child.type === 'JSXText' && child.value.trim()) {
49
+ firstTextChildIndex = i;
50
+ break;
51
+ } else if (child.type !== 'JSXText') {
52
+ break; // expression comes first — can't determine sentence start
53
+ }
54
+ }
55
+
56
+ node.children.forEach((child, index) => {
57
+ // Only check for JSXText (e.g., <Button>text</Button>)
58
+ if (child.type === 'JSXText') {
59
+ const text = child.value.trim();
60
+ if (!text) return;
61
+
62
+ // Compose the fixed text for both fixes if needed
63
+ let fixedText = text;
64
+
65
+ // Sentence case fix — only on the first visible text fragment
66
+ if (index === firstTextChildIndex && !checkSentenceCase(text)) {
67
+ fixedText = fixedText.charAt(0).toUpperCase() + fixedText.slice(1);
68
+ context.report({
69
+ node: child,
70
+ message: `Text in <${name}> should start with a capital letter (sentence case).`,
71
+ fix: fixer => fixer.replaceText(child, ' ' + fixedText + ' ')
72
+ });
73
+ }
74
+
75
+ // Resource name fix
76
+ const wrongResource = checkResourceNames(text);
77
+ if (wrongResource) {
78
+ const regex = new RegExp(`\\b${wrongResource.toLowerCase()}\\b`, 'g');
79
+ fixedText = fixedText.replace(regex, wrongResource);
80
+ context.report({
81
+ node: child,
82
+ message: `Resource name "${wrongResource}" in <${name}> must be capitalized.`,
83
+ fix: fixer => fixer.replaceText(child, fixedText)
84
+ });
85
+ }
86
+ }
87
+ });
88
+ }
89
+ };
90
+ }
91
+ };
92
+
93
+ export default technicalSentenceCase;