-
Notifications
You must be signed in to change notification settings - Fork 380
change: [UIE-8744] - Replace text field in DBaaS create page with Akamai CDS text field Web Component #12225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@linode/manager": Changed | ||
--- | ||
|
||
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)) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import * as React from 'react'; | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
||
import { TextFieldWrapper } from './TextFieldWrapper'; | ||
|
||
describe('TextFieldWrapper', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: It would be nice if we can resolve all the lint warnings in this file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ I agree with this, particularly with regards to the warnings about the unnecessary |
||
const props = { | ||
label: 'Cluster Label', | ||
value: 'test', | ||
}; | ||
|
||
it('Renders a TextField with the given label and initial value', async () => { | ||
const { getByTestId, getByText } = renderWithTheme( | ||
<TextFieldWrapper {...props} /> | ||
); | ||
const textFieldHost = await getByTestId('textfield-input'); | ||
Check warning on line 18 in packages/manager/src/features/Databases/TextFieldWrapper.test.tsx
|
||
const shadowTextField = textFieldHost?.shadowRoot?.querySelector('input'); | ||
|
||
expect(getByText('Cluster Label')).toBeInTheDocument(); | ||
Check warning on line 21 in packages/manager/src/features/Databases/TextFieldWrapper.test.tsx
|
||
expect(shadowTextField).toHaveValue('test'); | ||
}); | ||
|
||
it('Renders an error message on error', async () => { | ||
const { getByText } = renderWithTheme( | ||
<TextFieldWrapper errorText="There was an error" {...props} /> | ||
); | ||
expect(getByText(/There was an error/i)).toBeInTheDocument(); | ||
Check warning on line 29 in packages/manager/src/features/Databases/TextFieldWrapper.test.tsx
|
||
}); | ||
|
||
it('text field should be disabled on disable', async () => { | ||
const { getByTestId } = renderWithTheme( | ||
<TextFieldWrapper disabled {...props} /> | ||
); | ||
const textFieldHost = await getByTestId('textfield-input'); | ||
Check warning on line 36 in packages/manager/src/features/Databases/TextFieldWrapper.test.tsx
|
||
|
||
expect(textFieldHost).toBeDisabled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import { convertToKebabCase } from '@linode/ui'; | ||
import { Box, FormHelperText, InputLabel, TooltipIcon } from '@linode/ui'; | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's consolidate these imports There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grevanak-akamai do you have prettier and eslint plugins? You can format on save. |
||
import { useTheme } from '@mui/material/styles'; | ||
import { TextField } from 'akamai-cds-react-components'; | ||
import React, { useId, useRef } from 'react'; | ||
|
||
interface BaseProps { | ||
disabled?: boolean; | ||
/** | ||
* When defined, makes the input show an error state with the defined text | ||
*/ | ||
errorText?: string; | ||
label: string; | ||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; | ||
value?: Value; | ||
} | ||
|
||
type Value = string | undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit to having this type over doing the below on L15 (aside from changing the type on L29 from
|
||
|
||
interface InputToolTipProps { | ||
tooltipText?: JSX.Element | string; | ||
} | ||
|
||
type TextFieldWrapperProps = BaseProps & InputToolTipProps; | ||
|
||
export const TextFieldWrapper = (props: TextFieldWrapperProps) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than creating a wrapper we should replace the source directly in order to get an accurate performance test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nvm I see this mimics the TextField. |
||
const { disabled, errorText, label, onChange, tooltipText, value } = props; | ||
|
||
const [_value, setValue] = React.useState<Value>(value ?? ''); | ||
const theme = useTheme(); | ||
const inputRef = useRef<HTMLInputElement | null>(null); // Ref for the input field | ||
|
||
const isControlled = value !== undefined; | ||
|
||
const useFieldIds = ({ | ||
hasError = false, | ||
label, | ||
}: { | ||
hasError?: boolean; | ||
label: string; | ||
}) => { | ||
const fallbackId = useId(); | ||
const validInputId = label ? convertToKebabCase(label) : fallbackId; | ||
const helperTextId = `${validInputId}-helper-text`; | ||
const errorTextId = `${validInputId}-error-text`; | ||
const errorScrollClassName = hasError ? `error-for-scroll` : ''; | ||
|
||
return { | ||
errorScrollClassName, | ||
errorTextId, | ||
helperTextId, | ||
validInputId, | ||
}; | ||
}; | ||
|
||
const { errorScrollClassName, errorTextId, validInputId } = useFieldIds({ | ||
hasError: Boolean(errorText), | ||
label, | ||
}); | ||
|
||
React.useEffect(() => { | ||
if (isControlled) { | ||
setValue(value); | ||
} | ||
}, [value, isControlled]); | ||
|
||
// Simulate htmlFor for the label as it doesn't work with shadow DOM | ||
const handleLabelClick = () => { | ||
if (inputRef.current) { | ||
const shadowRoot = inputRef.current.shadowRoot; | ||
if (shadowRoot) { | ||
const inputElement = shadowRoot.querySelector('input'); | ||
if (inputElement) { | ||
(inputElement as HTMLElement).focus(); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
return ( | ||
<Box | ||
className={`${errorText ? errorScrollClassName : ''}`} | ||
sx={{ | ||
...(Boolean(tooltipText) && { | ||
alignItems: 'flex-end', | ||
display: 'flex', | ||
flexWrap: 'wrap', | ||
}), | ||
}} | ||
> | ||
<Box | ||
alignItems={'center'} | ||
data-testid="inputLabelWrapper" | ||
display="flex" | ||
sx={{ | ||
marginBottom: theme.spacingFunction(8), | ||
marginTop: theme.spacingFunction(16), | ||
}} | ||
> | ||
<InputLabel | ||
data-qa-textfield-label={label} | ||
htmlFor={validInputId} | ||
onClick={handleLabelClick} | ||
sx={{ | ||
marginBottom: 0, | ||
transform: 'none', | ||
}} | ||
> | ||
{label} | ||
</InputLabel> | ||
</Box> | ||
<Box | ||
sx={{ | ||
...(Boolean(tooltipText) && { | ||
display: 'flex', | ||
width: '100%', | ||
}), | ||
}} | ||
> | ||
<Box | ||
sx={{ | ||
width: '416px', | ||
minWidth: '120px', | ||
}} | ||
> | ||
<TextField | ||
aria-errormessage={errorText ? errorTextId : undefined} | ||
aria-invalid={!!errorText} | ||
className={errorText ? 'error' : ''} | ||
data-testid="textfield-input" | ||
disabled={disabled} | ||
id={validInputId} | ||
onChange={(e) => | ||
onChange?.(e as unknown as React.ChangeEvent<HTMLInputElement>) | ||
} | ||
ref={inputRef as React.RefObject<never>} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The castings here are concerning. Using web components shouldn't weaken the type safety of the application. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @grevanak-akamai can we fix this upstream in the components by correctly typing the events? |
||
value={_value} | ||
/> | ||
</Box> | ||
{tooltipText && ( | ||
<TooltipIcon | ||
status="help" | ||
sxTooltipIcon={{ | ||
height: '34px', | ||
margin: '0px 0px 0px 4px', | ||
padding: '17px', | ||
width: '34px', | ||
}} | ||
text={tooltipText} | ||
/> | ||
)} | ||
</Box> | ||
{errorText && ( | ||
<FormHelperText | ||
data-qa-textfield-error-text={label} | ||
role="alert" | ||
sx={{ | ||
alignItems: 'center', | ||
color: theme.palette.error.dark, | ||
display: 'flex', | ||
left: 5, | ||
top: 42, | ||
width: '100%', | ||
}} | ||
> | ||
{errorText} | ||
</FormHelperText> | ||
)} | ||
</Box> | ||
); | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.