Skip to content

Commit f054b22

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

File tree

5 files changed

+229
-12
lines changed

5 files changed

+229
-12
lines changed

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

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)