Skip to content

Commit 9707dbb

Browse files
change: [UIE-8744] - Replace text field in DBaaS create page with akamai CDS text field web component
1 parent 58ca5da commit 9707dbb

File tree

6 files changed

+230
-12
lines changed

6 files changed

+230
-12
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
DBaaS: Replace the text field component under DBAAS create DB cluster page with Akamai CDS text field web component ([#12225](https://github.com/linode/manager/pull/12225))

packages/manager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@tanstack/react-query-devtools": "5.51.24",
4646
"@tanstack/react-router": "^1.111.11",
4747
"@xterm/xterm": "^5.5.0",
48-
"akamai-cds-react-components": "0.0.1-alpha.6",
48+
"akamai-cds-react-components": "0.0.1-alpha.8",
4949
"algoliasearch": "^4.14.3",
5050
"axios": "~1.8.3",
5151
"braintree-web": "^3.92.2",

packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.style.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import { Box, Button, TextField, Typography } from '@linode/ui';
1+
import { Box, Button, Typography } from '@linode/ui';
22
import { Grid, styled } from '@mui/material';
33

44
import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';
55

6+
import { TextFieldWrapper } from '../TextFieldWrapper';
7+
68
export const StyledLabelTooltip = styled(Box, {
79
label: 'StyledLabelTooltip',
810
})(() => ({
@@ -14,7 +16,7 @@ export const StyledLabelTooltip = styled(Box, {
1416
},
1517
}));
1618

17-
export const StyledTextField = styled(TextField, {
19+
export const StyledTextField = styled(TextFieldWrapper, {
1820
label: 'StyledTextField',
1921
})(({ theme }) => ({
2022
'& .MuiTooltip-tooltip': {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as React from 'react';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { TextFieldWrapper } from './TextFieldWrapper';
7+
8+
describe('TextFieldWrapper', () => {
9+
const props = {
10+
label: 'Cluster Label',
11+
value: 'test',
12+
};
13+
14+
it('Renders a TextField with the given label and initial value', async () => {
15+
const { getByTestId, getByText } = renderWithTheme(
16+
<TextFieldWrapper {...props} />
17+
);
18+
const textFieldHost = await getByTestId('textfield-input');
19+
const shadowTextField = textFieldHost?.shadowRoot?.querySelector('input');
20+
21+
expect(getByText('Cluster Label')).toBeInTheDocument();
22+
expect(shadowTextField).toHaveValue('test');
23+
});
24+
25+
it('Renders an error message on error', async () => {
26+
const { getByText } = renderWithTheme(
27+
<TextFieldWrapper errorText="There was an error" {...props} />
28+
);
29+
expect(getByText(/There was an error/i)).toBeInTheDocument();
30+
});
31+
32+
it('text field should be disabled on disable', async () => {
33+
const { getByTestId } = renderWithTheme(
34+
<TextFieldWrapper disabled {...props} />
35+
);
36+
const textFieldHost = await getByTestId('textfield-input');
37+
38+
expect(textFieldHost).toBeDisabled();
39+
});
40+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { convertToKebabCase } from '@linode/ui';
2+
import { Box, FormHelperText, InputLabel, TooltipIcon } from '@linode/ui';
3+
import { useTheme } from '@mui/material/styles';
4+
import { TextField } from 'akamai-cds-react-components';
5+
import React, { useId, useRef } from 'react';
6+
7+
interface BaseProps {
8+
disabled?: boolean;
9+
/**
10+
* When defined, makes the input show an error state with the defined text
11+
*/
12+
errorText?: string;
13+
label: string;
14+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
15+
value?: Value;
16+
}
17+
18+
type Value = string | undefined;
19+
20+
interface InputToolTipProps {
21+
tooltipText?: JSX.Element | string;
22+
}
23+
24+
type TextFieldWrapperProps = BaseProps & InputToolTipProps;
25+
26+
export const TextFieldWrapper = (props: TextFieldWrapperProps) => {
27+
const { disabled, errorText, label, onChange, tooltipText, value } = props;
28+
29+
const [_value, setValue] = React.useState<Value>(value ?? '');
30+
const theme = useTheme();
31+
const inputRef = useRef<HTMLInputElement | null>(null); // Ref for the input field
32+
33+
const isControlled = value !== undefined;
34+
35+
const useFieldIds = ({
36+
hasError = false,
37+
label,
38+
}: {
39+
hasError?: boolean;
40+
label: string;
41+
}) => {
42+
const fallbackId = useId();
43+
const validInputId = label ? convertToKebabCase(label) : fallbackId;
44+
const helperTextId = `${validInputId}-helper-text`;
45+
const errorTextId = `${validInputId}-error-text`;
46+
const errorScrollClassName = hasError ? `error-for-scroll` : '';
47+
48+
return {
49+
errorScrollClassName,
50+
errorTextId,
51+
helperTextId,
52+
validInputId,
53+
};
54+
};
55+
56+
const { errorScrollClassName, errorTextId, validInputId } = useFieldIds({
57+
hasError: Boolean(errorText),
58+
label,
59+
});
60+
61+
React.useEffect(() => {
62+
if (isControlled) {
63+
setValue(value);
64+
}
65+
}, [value, isControlled]);
66+
67+
// Simulate htmlFor for the label as it doesn't work with shadow DOM
68+
const handleLabelClick = () => {
69+
if (inputRef.current) {
70+
const shadowRoot = inputRef.current.shadowRoot;
71+
if (shadowRoot) {
72+
const inputElement = shadowRoot.querySelector('input');
73+
if (inputElement) {
74+
(inputElement as HTMLElement).focus();
75+
}
76+
}
77+
}
78+
};
79+
80+
return (
81+
<Box
82+
className={`${errorText ? errorScrollClassName : ''}`}
83+
sx={{
84+
...(Boolean(tooltipText) && {
85+
alignItems: 'flex-end',
86+
display: 'flex',
87+
flexWrap: 'wrap',
88+
}),
89+
}}
90+
>
91+
<Box
92+
alignItems={'center'}
93+
data-testid="inputLabelWrapper"
94+
display="flex"
95+
sx={{
96+
marginBottom: theme.spacingFunction(8),
97+
marginTop: theme.spacingFunction(16),
98+
}}
99+
>
100+
<InputLabel
101+
data-qa-textfield-label={label}
102+
htmlFor={validInputId}
103+
onClick={handleLabelClick}
104+
sx={{
105+
marginBottom: 0,
106+
transform: 'none',
107+
}}
108+
>
109+
{label}
110+
</InputLabel>
111+
</Box>
112+
<Box
113+
sx={{
114+
...(Boolean(tooltipText) && {
115+
display: 'flex',
116+
width: '100%',
117+
}),
118+
}}
119+
>
120+
<Box
121+
sx={{
122+
width: '416px',
123+
minWidth: '120px',
124+
}}
125+
>
126+
<TextField
127+
aria-errormessage={errorText ? errorTextId : undefined}
128+
aria-invalid={!!errorText}
129+
className={errorText ? 'error' : ''}
130+
data-testid="textfield-input"
131+
disabled={disabled}
132+
id={validInputId}
133+
onChange={(e) =>
134+
onChange?.(e as unknown as React.ChangeEvent<HTMLInputElement>)
135+
}
136+
ref={inputRef as React.RefObject<never>}
137+
value={_value}
138+
/>
139+
</Box>
140+
{tooltipText && (
141+
<TooltipIcon
142+
status="help"
143+
sxTooltipIcon={{
144+
height: '34px',
145+
margin: '0px 0px 0px 4px',
146+
padding: '17px',
147+
width: '34px',
148+
}}
149+
text={tooltipText}
150+
/>
151+
)}
152+
</Box>
153+
{errorText && (
154+
<FormHelperText
155+
data-qa-textfield-error-text={label}
156+
role="alert"
157+
sx={{
158+
alignItems: 'center',
159+
color: theme.palette.error.dark,
160+
display: 'flex',
161+
left: 5,
162+
top: 42,
163+
width: '100%',
164+
}}
165+
>
166+
{errorText}
167+
</FormHelperText>
168+
)}
169+
</Box>
170+
);
171+
};

pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)