From 91bf8972c23a253f46271a9fc809240270338141 Mon Sep 17 00:00:00 2001 From: Henri Bernard <132286421+hebernardEquisoft@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:28:59 -0400 Subject: [PATCH 01/23] chore(form-field): form refactor --- .../src/components/combobox/combobox.tsx | 12 +- .../components/date-picker/date-picker.tsx | 8 +- .../dropdown-list/dropdown-list.tsx | 8 +- .../field-container/field-container.test.tsx | 40 --- .../field-container.test.tsx.snap | 202 ----------- .../field-container/field-container.tsx | 94 ------ .../feedbacks/invalid-field.test.tsx | 2 +- .../feedbacks/invalid-field.test.tsx.snap | 2 +- .../{ => form}/feedbacks/invalid-field.tsx | 8 +- .../form-field-container.test.tsx | 318 ++++++++++++++++++ .../form-field-container.test.tsx.snap | 241 +++++++++++++ .../form-container/form-field-container.tsx | 134 ++++++++ .../src/components/form/form-field-context.ts | 39 +++ packages/react/src/components/form/utils.ts | 71 ++++ .../numeric-input/numeric-input.tsx | 8 +- .../password-creation-input.tsx | 6 +- .../password-input/password-input.tsx | 8 +- .../stepper-input/stepper-input.tsx | 8 +- .../src/components/text-area/text-area.tsx | 8 +- .../components/text-input/styles/inputs.tsx | 84 ++++- .../src/components/text-input/text-input.tsx | 8 +- 21 files changed, 928 insertions(+), 381 deletions(-) delete mode 100644 packages/react/src/components/field-container/field-container.test.tsx delete mode 100644 packages/react/src/components/field-container/field-container.test.tsx.snap delete mode 100644 packages/react/src/components/field-container/field-container.tsx rename packages/react/src/components/{ => form}/feedbacks/invalid-field.test.tsx (81%) rename packages/react/src/components/{ => form}/feedbacks/invalid-field.test.tsx.snap (97%) rename packages/react/src/components/{ => form}/feedbacks/invalid-field.tsx (86%) create mode 100644 packages/react/src/components/form/form-container/form-field-container.test.tsx create mode 100644 packages/react/src/components/form/form-container/form-field-container.test.tsx.snap create mode 100644 packages/react/src/components/form/form-container/form-field-container.tsx create mode 100644 packages/react/src/components/form/form-field-context.ts create mode 100644 packages/react/src/components/form/utils.ts diff --git a/packages/react/src/components/combobox/combobox.tsx b/packages/react/src/components/combobox/combobox.tsx index 971999d4c1..3d38545726 100644 --- a/packages/react/src/components/combobox/combobox.tsx +++ b/packages/react/src/components/combobox/combobox.tsx @@ -15,7 +15,7 @@ import { useTranslation } from '../../i18n/use-translation'; import { ResolvedTheme } from '../../themes/theme'; import { focus } from '../../utils/css-state'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { IconButton } from '../buttons/icon-button'; import { Listbox, ListboxOption } from '../listbox/listbox'; import { TooltipProps } from '../tooltip/tooltip'; @@ -46,7 +46,7 @@ function getBorderColor({ disabled, theme, $valid }: TextboxProps): string { return theme.component['combobox-border-color']; } -const StyledFieldContainer = styled(FieldContainer)` +const StyledFieldContainer = styled(FormFieldContainer)` position: relative; `; @@ -75,7 +75,7 @@ const Textbox = styled.input` padding: 0 var(--spacing-1x); width: 100%; ${({ theme }) => focus({ theme }, true)}; - + &::placeholder { color: ${({ theme }) => theme.greys['mid-grey']}; font-style: italic; @@ -109,14 +109,14 @@ const ClearButton = styled(IconButton)<{ disabled?: boolean }>` position: absolute; right: calc(var(--size-1x) + var(--spacing-1halfx)); width: var(--size-1x); - + &::after { border-right: ${({ theme }) => `1px solid ${theme.greys['mid-grey']}`}; content: ''; height: calc(var(--size-2x) - var(--spacing-2x)); margin-left: var(--spacing-1x); } - + &:hover { background-color: transparent; } @@ -593,7 +593,7 @@ export const Combobox: VoidFunctionComponent = ({ - - + ); }); diff --git a/packages/react/src/components/dropdown-list/dropdown-list.tsx b/packages/react/src/components/dropdown-list/dropdown-list.tsx index a0327c7378..6e6b6b61f9 100644 --- a/packages/react/src/components/dropdown-list/dropdown-list.tsx +++ b/packages/react/src/components/dropdown-list/dropdown-list.tsx @@ -14,7 +14,7 @@ import { ResolvedTheme } from '../../themes/theme'; import { focus } from '../../utils/css-state'; import { isLetterOrNumber } from '../../utils/regex'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { Icon } from '../icon/icon'; import { Listbox, ListboxOption } from '../listbox/listbox'; import { TooltipProps } from '../tooltip/tooltip'; @@ -52,7 +52,7 @@ function getBorderColor({ $disabled, theme, $valid }: TextboxProps): string { return theme.component['dropdown-list-input-border-color']; } -const StyledFieldContainer = styled(FieldContainer)` +const StyledFieldContainer = styled(FormFieldContainer)` position: relative; `; @@ -95,7 +95,7 @@ const TagWrapper = styled.div` const ListBoxTag = styled(Tag)` margin: 2px; - + & + & { margin-left: 2px; } @@ -462,7 +462,7 @@ export const DropdownList: VoidFunctionComponent { - test('matches snapshot', () => { - const tree = renderWithProviders( - - Children - , - ); - - expect(tree).toMatchSnapshot(); - }); - - test('should have invalid styles given valid prop is set to false', () => { - const tree = renderWithProviders( - - Children - , - ); - - expect(tree).toMatchSnapshot(); - }); - - test('should not have margins given noMargin prop is set to true', () => { - const tree = renderWithProviders( - - Children - , - ); - - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/packages/react/src/components/field-container/field-container.test.tsx.snap b/packages/react/src/components/field-container/field-container.test.tsx.snap deleted file mode 100644 index 522105761b..0000000000 --- a/packages/react/src/components/field-container/field-container.test.tsx.snap +++ /dev/null @@ -1,202 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Field Container matches snapshot 1`] = ` -.c2 { - color: #000000; - display: block; - font-size: 0.75rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.02rem; - -moz-letter-spacing: 0.02rem; - -ms-letter-spacing: 0.02rem; - letter-spacing: 0.02rem; - line-height: 1.25rem; - margin: 0; - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -input + .c1 { - margin-left: var(--spacing-half); -} - -.c0 { - margin: 0 0 var(--spacing-3x); -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #60666E; -} - -.c0:focus { - border-color: #006296; -} - -.c0 > :nth-child(1) { - margin-bottom: var(--spacing-half); -} - -
- - Children -
-`; - -exports[`Field Container should have invalid styles given valid prop is set to false 1`] = ` -.c3 { - color: #CD2C23; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - font-size: 0.75rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.02rem; - -moz-letter-spacing: 0.02rem; - -ms-letter-spacing: 0.02rem; - letter-spacing: 0.02rem; - line-height: 1.25rem; -} - -.c4 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - margin-right: var(--spacing-base); -} - -.c2 { - color: #000000; - display: block; - font-size: 0.75rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.02rem; - -moz-letter-spacing: 0.02rem; - -ms-letter-spacing: 0.02rem; - letter-spacing: 0.02rem; - line-height: 1.25rem; - margin: 0; - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -input + .c1 { - margin-left: var(--spacing-half); -} - -.c0 { - margin: 0 0 var(--spacing-3x); -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #CD2C23; -} - -.c0:focus { - border-color: #CD2C23; -} - -.c0 > :nth-child(2) { - margin-bottom: var(--spacing-half); -} - -
- - - - This text area input is invalid - - Children -
-`; - -exports[`Field Container should not have margins given noMargin prop is set to true 1`] = ` -.c2 { - color: #000000; - display: block; - font-size: 0.75rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.02rem; - -moz-letter-spacing: 0.02rem; - -ms-letter-spacing: 0.02rem; - letter-spacing: 0.02rem; - line-height: 1.25rem; - margin: 0; - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; -} - -input + .c1 { - margin-left: var(--spacing-half); -} - -.c0 { - margin: 0; -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #60666E; -} - -.c0:focus { - border-color: #006296; -} - -.c0 > :nth-child(1) { - margin-bottom: var(--spacing-half); -} - -
- - Children -
-`; diff --git a/packages/react/src/components/field-container/field-container.tsx b/packages/react/src/components/field-container/field-container.tsx deleted file mode 100644 index b443214c3f..0000000000 --- a/packages/react/src/components/field-container/field-container.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { FunctionComponent, PropsWithChildren } from 'react'; -import styled from 'styled-components'; -import { ResolvedTheme } from '../../themes/theme'; -import { useDeviceContext } from '../device-context-provider/device-context-provider'; -import { InvalidField } from '../feedbacks/invalid-field'; -import { Label } from '../label/label'; -import { TooltipProps } from '../tooltip/tooltip'; - -interface StyledDivProps { - theme: ResolvedTheme; - hasLabel: boolean; - hasHint: boolean; - valid: boolean; - noMargin?: boolean; -} - -const StyledDiv = styled.div` - margin: ${({ noMargin }) => (noMargin ? '0' : '0 0 var(--spacing-3x)')}; - - input, - select, - textarea { - border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; - } - - &:focus { - border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-focus-border-color'] : theme.component['field-input-error-focus-border-color'])}; - } - - > :nth-child(${({ hasLabel, hasHint, valid }) => (hasLabel ? 1 : 0) + (hasHint ? 1 : 0) + (!valid ? 1 : 0)}) { - margin-bottom: var(--spacing-half); - } -`; - -const StyledHint = styled.span<{ isMobile: boolean }>` - color: ${(props) => props.theme.component['field-hint-text-color']}; - display: block; - font-size: ${({ isMobile }) => (isMobile ? '0.875rem' : '0.75rem')}; - font-weight: var(--font-normal); - letter-spacing: 0.02rem; - line-height: ${({ isMobile }) => (isMobile ? '1.5rem' : '1.25rem')}; -`; - -export interface FieldContainerProps { - className?: string; - fieldId: string; - hint?: string; - label?: string; - noInvalidFieldIcon?: boolean; - noMargin?: boolean; - required?: boolean; - tooltip?: TooltipProps; - valid: boolean; - validationErrorMessage: string; -} - -export const FieldContainer: FunctionComponent> = ({ - children, - className, - fieldId, - hint, - label, - noInvalidFieldIcon, - noMargin, - required, - tooltip, - valid, - validationErrorMessage, - ...props -}) => { - const { isMobile } = useDeviceContext(); - - return ( - - {label && } - {hint && {hint}} - {!valid && ( - - )} - {children} - - ); -}; diff --git a/packages/react/src/components/feedbacks/invalid-field.test.tsx b/packages/react/src/components/form/feedbacks/invalid-field.test.tsx similarity index 81% rename from packages/react/src/components/feedbacks/invalid-field.test.tsx rename to packages/react/src/components/form/feedbacks/invalid-field.test.tsx index 4f4a8ec302..8a625efd6f 100644 --- a/packages/react/src/components/feedbacks/invalid-field.test.tsx +++ b/packages/react/src/components/form/feedbacks/invalid-field.test.tsx @@ -1,4 +1,4 @@ -import { renderWithTheme } from '../../test-utils/renderer'; +import { renderWithTheme } from '../../../test-utils/renderer'; import { InvalidField } from './invalid-field'; describe('Invalid field', () => { diff --git a/packages/react/src/components/feedbacks/invalid-field.test.tsx.snap b/packages/react/src/components/form/feedbacks/invalid-field.test.tsx.snap similarity index 97% rename from packages/react/src/components/feedbacks/invalid-field.test.tsx.snap rename to packages/react/src/components/form/feedbacks/invalid-field.test.tsx.snap index d047f8c241..e4d6ab8723 100644 --- a/packages/react/src/components/feedbacks/invalid-field.test.tsx.snap +++ b/packages/react/src/components/form/feedbacks/invalid-field.test.tsx.snap @@ -31,7 +31,7 @@ exports[`Invalid field Matches the snapshot 1`] = ` aria-live="polite" class="c0" data-testid="invalid-field" - id="test-id_invalid" + id="test-id" role="alert" > ` color: ${(props) => props.theme.component['field-error-text-color']}; @@ -19,7 +19,7 @@ const StyledIcon = styled(Icon)` `; interface InvalidFieldProps { - controlId: string; + controlId?: string; feedbackMsg: string; noIcon?: boolean; } @@ -31,7 +31,7 @@ const InvalidField: VoidFunctionComponent = ({ controlId, fee diff --git a/packages/react/src/components/form/form-container/form-field-container.test.tsx b/packages/react/src/components/form/form-container/form-field-container.test.tsx new file mode 100644 index 0000000000..cc0b982b23 --- /dev/null +++ b/packages/react/src/components/form/form-container/form-field-container.test.tsx @@ -0,0 +1,318 @@ +import { renderWithProviders } from '../../../test-utils/renderer'; +import { devConsole } from '../../../utils/dev-console'; +import { Input } from '../../text-input/styles/inputs'; +import { ARIA_LABEL_WARNING } from '../utils'; +import { FormFieldContainer } from './form-field-container'; + +describe('Form Field Container', () => { + describe('Styling', () => { + const defaultProps = { + id: 'id', + validationErrorMessage: 'This text area input is invalid', + }; + + test('matches default snapshot', () => { + const tree = renderWithProviders( + + Children + , + ); + + expect(tree).toMatchSnapshot(); + }); + + test('matches invalid snapshot', () => { + const tree = renderWithProviders( + + Children + , + ); + + expect(tree).toMatchSnapshot(); + }); + + test('match noMargin snapshot', () => { + const tree = renderWithProviders( + + Children + , + ); + + expect(tree).toMatchSnapshot(); + }); + }); + + describe('Accessibility', () => { + const label = 'This is a label.'; + const ariaLabel = 'This is a ariaLabel.'; + const providedId = 'id'; + const hint = 'This is a hint.'; + const invalidMessage = 'This is invalid.'; + const additionalElementId = 'idAdditionalElement'; + + test('should log if one of label props is missing', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(undefined); + expect(inputWrapper.prop('aria-describedby')).toBe(undefined); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(ARIA_LABEL_WARNING); + consoleSpy.mockRestore(); + }); + + test('should log if more than one label props is provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label ${additionalElementId}`); + expect(inputWrapper.prop('aria-describedby')).toBe(`${providedId}_hint ${additionalElementId}`); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(ARIA_LABEL_WARNING); + consoleSpy.mockRestore(); + }); + + test('should override parent label props if provided to child input', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(`${ariaLabel}_Child`); + expect(inputWrapper.prop('aria-labelledby')).toBe(undefined); + expect(inputWrapper.prop('aria-describedby')).toBe(`${additionalElementId}_Child`); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(ARIA_LABEL_WARNING); + consoleSpy.mockRestore(); + }); + + describe('aria-label', () => { + test('should log when both label and ariaLabel are provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(undefined); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(ARIA_LABEL_WARNING); + consoleSpy.mockRestore(); + }); + }); + + describe('aria-labelledby', () => { + test('should contain idLabel when label provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(undefined); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + + test('should contain idAdditionalElement when ariaLabelledBy provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${additionalElementId}`); + expect(inputWrapper.prop('aria-describedby')).toBe(undefined); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + }); + + describe('aria-describedby', () => { + test('should contain idHint when hint provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(`${providedId}_hint`); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + + test('shouldn\'t contain idInvalid when FormContainer is valid', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(undefined); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + + test('should contain idInvalid when FormContainer is invalid', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(`${providedId}_invalid`); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + + test('should contain idAdditionalElement when ariaDescribedBy provided', () => { + const consoleSpy = jest.spyOn(devConsole, 'warn'); + consoleSpy.mockImplementation(() => {}); + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-label')).toBe(undefined); + expect(inputWrapper.prop('aria-labelledby')).toBe(`${providedId}_label`); + expect(inputWrapper.prop('aria-describedby')).toBe(`${providedId}_hint ${additionalElementId}`); + expect(consoleSpy).toHaveBeenCalledTimes(0); + consoleSpy.mockRestore(); + }); + }); + + describe('aria-invalid', () => { + test('should be true when Formfield is invalid', () => { + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-invalid')).toBe('true'); + }); + }); + + describe('aria-required', () => { + test('should be true when Formfield is required', () => { + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-required')).toBe('true'); + }); + }); + + describe('aria-disabled', () => { + test('should be true when FormContainer is disabled', () => { + const wrapper = renderWithProviders( + + + , + ); + + const inputWrapper = wrapper.find('input'); + expect(inputWrapper.prop('aria-disabled')).toBe('true'); + }); + }); + }); +}); diff --git a/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap new file mode 100644 index 0000000000..8877ef1137 --- /dev/null +++ b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap @@ -0,0 +1,241 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Form Field Container Styling match noMargin snapshot 1`] = ` +.c0 { + margin: 0; +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #60666E; +} + +.c0:focus { + border-color: #006296; +} + +.c0 > :nth-child(0) { + margin-bottom: var(--spacing-half); +} + +
+ Children +
+`; + +exports[`Form Field Container Styling matches default snapshot 1`] = ` +.c0 { + margin: 0 0 var(--spacing-3x); +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #60666E; +} + +.c0:focus { + border-color: #006296; +} + +.c0 > :nth-child(0) { + margin-bottom: var(--spacing-half); +} + +
+ Children +
+`; + +exports[`Form Field Container Styling matches invalid snapshot 1`] = ` +.c1 { + color: #CD2C23; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-size: 0.75rem; + font-weight: var(--font-normal); + -webkit-letter-spacing: 0.02rem; + -moz-letter-spacing: 0.02rem; + -ms-letter-spacing: 0.02rem; + letter-spacing: 0.02rem; + line-height: 1.25rem; +} + +.c2 { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + margin-right: var(--spacing-base); +} + +.c0 { + margin: 0 0 var(--spacing-3x); +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #CD2C23; +} + +.c0:focus { + border-color: #CD2C23; +} + +.c0 > :nth-child(1) { + margin-bottom: var(--spacing-half); +} + +
+ + + This text area input is invalid + + Children +
+`; + +exports[`Form Field Styling match noMargin snapshot 1`] = ` +.c0 { + margin: 0; +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #60666E; +} + +.c0:focus { + border-color: #006296; +} + +.c0 > :nth-child(0) { + margin-bottom: var(--spacing-half); +} + +
+ Children +
+`; + +exports[`Form Field Styling matches default snapshot 1`] = ` +.c0 { + margin: 0 0 var(--spacing-3x); +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #60666E; +} + +.c0:focus { + border-color: #006296; +} + +.c0 > :nth-child(0) { + margin-bottom: var(--spacing-half); +} + +
+ Children +
+`; + +exports[`Form Field Styling matches invalid snapshot 1`] = ` +.c1 { + color: #CD2C23; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-size: 0.75rem; + font-weight: var(--font-normal); + -webkit-letter-spacing: 0.02rem; + -moz-letter-spacing: 0.02rem; + -ms-letter-spacing: 0.02rem; + letter-spacing: 0.02rem; + line-height: 1.25rem; +} + +.c2 { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + margin-right: var(--spacing-base); +} + +.c0 { + margin: 0 0 var(--spacing-3x); +} + +.c0 input, +.c0 select, +.c0 textarea { + border-color: #CD2C23; +} + +.c0:focus { + border-color: #CD2C23; +} + +.c0 > :nth-child(1) { + margin-bottom: var(--spacing-half); +} + +
+ + + This text area input is invalid + + Children +
+`; diff --git a/packages/react/src/components/form/form-container/form-field-container.tsx b/packages/react/src/components/form/form-container/form-field-container.tsx new file mode 100644 index 0000000000..0168406e72 --- /dev/null +++ b/packages/react/src/components/form/form-container/form-field-container.tsx @@ -0,0 +1,134 @@ +import { FunctionComponent, PropsWithChildren, useMemo } from 'react'; +import styled from 'styled-components'; +import { useId } from '../../../hooks/use-id'; +import { ResolvedTheme } from '../../../themes/theme'; +import { useDeviceContext } from '../../device-context-provider/device-context-provider'; +import { InvalidField } from '../feedbacks/invalid-field'; +import { Label } from '../../label/label'; +import { TooltipProps } from '../../tooltip/tooltip'; +import { FormFieldContext, FormFieldControlProps } from '../form-field-context'; +import { getAriaDescribedBy, getAriaLabel, getAriaLabelledBy, getSlotIds } from '../utils'; + +interface StyledDivProps { + theme: ResolvedTheme; + hasLabel: boolean; + hasHint: boolean; + valid: boolean; + noMargin?: boolean; +} + +const StyledDiv = styled.div` + margin: ${({ noMargin }) => (noMargin ? '0' : '0 0 var(--spacing-3x)')}; + + input, + select, + textarea { + border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; + } + + &:focus { + border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-focus-border-color'] : theme.component['field-input-error-focus-border-color'])}; + } + + > :nth-child(${({ hasLabel, hasHint, valid }) => (hasLabel ? 1 : 0) + (hasHint ? 1 : 0) + (!valid ? 1 : 0)}) { + margin-bottom: var(--spacing-half); + } +`; + +const StyledHint = styled.span<{ isMobile: boolean }>` + color: ${(props) => props.theme.component['field-hint-text-color']}; + display: block; + font-size: ${({ isMobile }) => (isMobile ? '0.875rem' : '0.75rem')}; + font-weight: var(--font-normal); + letter-spacing: 0.02rem; + line-height: ${({ isMobile }) => (isMobile ? '1.5rem' : '1.25rem')}; +`; + +export interface FormFieldContainerProps extends FormFieldControlProps { + className?: string; + id?: string; + noInvalidFieldIcon?: boolean; + noMargin?: boolean; + tooltip?: TooltipProps; + label?: string; + hint?: string; + validationErrorMessage: string; +} + +export const FormFieldContainer: FunctionComponent> = ({ + id: providedId, + ariaLabel: providedAriaLabel, + ariaLabelledby: providedAriaLabelledby, + ariaDescribedby: providedAriaDescribedby, + children, + className, + disabled = false, + hint, + label, + noInvalidFieldIcon, + noMargin, + required = false, + tooltip, + valid = true, + validationErrorMessage, + ...props +}) => { + const { isMobile } = useDeviceContext(); + const formId = useId(providedId); + const slotIds = getSlotIds(formId, label, hint, (validationErrorMessage && !valid)); + + const ariaLabel = getAriaLabel(label, providedAriaLabel, providedAriaLabelledby); + const ariaLabelledby = getAriaLabelledBy(slotIds, providedAriaLabelledby); + const ariaDescribedby = getAriaDescribedBy(slotIds, providedAriaDescribedby); + + const contextValues = useMemo(() => ({ + formId, + ariaLabel, + ariaLabelledby, + ariaDescribedby, + valid, + hint, + required, + disabled, + }), [formId, ariaLabel, ariaLabelledby, ariaDescribedby, valid, hint, required, disabled]); + + return ( + + + {label && ( + + )} + {hint && ( + + {hint} + + )} + {!valid && ( + + )} + {children} + + + ); +}; diff --git a/packages/react/src/components/form/form-field-context.ts b/packages/react/src/components/form/form-field-context.ts new file mode 100644 index 0000000000..d06906c66e --- /dev/null +++ b/packages/react/src/components/form/form-field-context.ts @@ -0,0 +1,39 @@ +import { createContext, useContext } from 'react'; + +export interface FormFieldControlProps { + formId?: string; + ariaLabel?: string; + ariaLabelledby?: string; + ariaDescribedby?: string + disabled?: boolean; + required?: boolean; + valid?: boolean; +} + +export const FormFieldContext = createContext({}); + +function applyDefault(fields: T, defaults: T, keys: (keyof T)[]): T { + const result = {}; + keys.forEach((key) => { + result[key] = fields[key] === undefined ? defaults[key] : fields[key]; + }); + return result; +} + +export function useFormFieldContext(props: FormFieldControlProps): FormFieldControlProps { + const context = useContext(FormFieldContext); + + return applyDefault( + props, + context, + [ + 'formId', + 'ariaLabel', + 'ariaLabelledby', + 'ariaDescribedby', + 'valid', + 'required', + 'disabled', + ], + ); +} diff --git a/packages/react/src/components/form/utils.ts b/packages/react/src/components/form/utils.ts new file mode 100644 index 0000000000..3fb12c1559 --- /dev/null +++ b/packages/react/src/components/form/utils.ts @@ -0,0 +1,71 @@ +import { devConsole } from '../../utils/dev-console'; + +interface FormFieldIds { + label?: string; + hint?: string; + invalid?: string; +} + +function makeSlotId(prop: React.ReactNode, formFieldId: string, propName: string): string | undefined { + if (!prop) { + return undefined; + } + + return `${formFieldId}_${propName}`; +} + +export function getSlotIds( + formFieldId: string, + label?: React.ReactNode, + hint?: React.ReactNode, + invalid?: React.ReactNode, +): FormFieldIds { + return { + label: makeSlotId(label, formFieldId, 'label'), + hint: makeSlotId(hint, formFieldId, 'hint'), + invalid: makeSlotId(invalid, formFieldId, 'invalid'), + }; +} + +export const ARIA_LABEL_WARNING = '' + + 'label, aria-label and aria-labelledby are mutually exclusive properties. Only one should be provided.'; + +export const validateAriaLabels = ( + label?: string, + ariaLabel?: string, + ariaLabelledby?: string, +): boolean => { + const labelCount = [!!label, !!ariaLabel, !!ariaLabelledby].filter(Boolean).length; + if (labelCount === 0 || labelCount > 1) { + devConsole.warn(ARIA_LABEL_WARNING); + return false; + } + return true; +}; + +export function joinStrings(...strings: Array): string | undefined { + const filteredStrings = strings.filter(Boolean); + return filteredStrings.length > 0 ? filteredStrings.join(' ') : undefined; +} + +export function getAriaLabel( + label?: string, + ariaLabel?: string, + ariaLabelledby?: string, +): string | undefined { + return validateAriaLabels(label, ariaLabel, ariaLabelledby) ? ariaLabel : undefined; +} + +export function getAriaLabelledBy( + { label }: FormFieldIds, + additionalLabelledBy?: string, +): string | undefined { + return joinStrings(label, additionalLabelledBy); +} + +export function getAriaDescribedBy( + { invalid, hint }: FormFieldIds, + additionalDescribedBy?: string, +): string | undefined { + return joinStrings(invalid, hint, additionalDescribedBy); +} diff --git a/packages/react/src/components/numeric-input/numeric-input.tsx b/packages/react/src/components/numeric-input/numeric-input.tsx index 637ea20cac..912f1a608b 100644 --- a/packages/react/src/components/numeric-input/numeric-input.tsx +++ b/packages/react/src/components/numeric-input/numeric-input.tsx @@ -10,7 +10,7 @@ import { useId } from '../../hooks/use-id'; import { focus } from '../../utils/css-state'; import { ResolvedTheme } from '../../themes/theme'; import { DeviceContextProps, useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { inputsStyle } from '../text-input/styles/inputs'; import { TooltipProps } from '../tooltip/tooltip'; import { useNumericInput, UseNumericInputParams } from './use-numeric-input'; @@ -144,9 +144,9 @@ export const NumericInput: VoidFunctionComponent = ({ ), [adornment, adornmentPosition]); return ( - = ({ /> {(adornment && adornmentPosition === 'end') && adornmentContent} - + ); }; diff --git a/packages/react/src/components/password-creation-input/password-creation-input.tsx b/packages/react/src/components/password-creation-input/password-creation-input.tsx index a42894dd0e..e6760af0bd 100644 --- a/packages/react/src/components/password-creation-input/password-creation-input.tsx +++ b/packages/react/src/components/password-creation-input/password-creation-input.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, useState, VoidFunctionComponent, useMemo } from 'react'; import styled, { StyledProps } from 'styled-components'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { IconButton } from '../buttons/icon-button'; import { TextInput } from '../text-input/text-input'; import { useTranslation } from '../../i18n/use-translation'; @@ -12,7 +12,7 @@ import { v4 as uuid } from '../../utils/uuid'; import { PasswordStrengthContainer } from './password-strength-container'; import { useDataAttributes } from '../../hooks/use-data-attributes'; -const StyledFieldContainer = styled(FieldContainer)` +const StyledFieldContainer = styled(FormFieldContainer)` > :nth-child(2) { margin-bottom: 0; } @@ -123,7 +123,7 @@ export const PasswordCreationInput: VoidFunctionComponent = ({ }, [onChange]); return ( - = ({ /> - + ); }; diff --git a/packages/react/src/components/stepper-input/stepper-input.tsx b/packages/react/src/components/stepper-input/stepper-input.tsx index 41bf948e75..0f102b0738 100644 --- a/packages/react/src/components/stepper-input/stepper-input.tsx +++ b/packages/react/src/components/stepper-input/stepper-input.tsx @@ -12,7 +12,7 @@ import { useTranslation } from '../../i18n/use-translation'; import { ResolvedTheme } from '../../themes/theme'; import { v4 as uuid } from '../../utils/uuid'; import { DeviceContextProps, useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { responsiveInputsStyle } from '../text-input/styles/inputs'; import { TooltipProps } from '../tooltip/tooltip'; import { StepperButtons } from './stepper-buttons'; @@ -126,8 +126,8 @@ export const StepperInput: VoidFunctionComponent = ({ }; return ( - = ({ /> )} - + ); }; diff --git a/packages/react/src/components/text-area/text-area.tsx b/packages/react/src/components/text-area/text-area.tsx index 23f296e849..d6cb603d72 100644 --- a/packages/react/src/components/text-area/text-area.tsx +++ b/packages/react/src/components/text-area/text-area.tsx @@ -4,7 +4,7 @@ import { useDataAttributes } from '../../hooks/use-data-attributes'; import { useTranslation } from '../../i18n/use-translation'; import { ResolvedTheme } from '../../themes/theme'; import { v4 as uuid } from '../../utils/uuid'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { inputsStyle } from '../text-input/styles/inputs'; import { TooltipProps } from '../tooltip/tooltip'; import { ScreenReaderOnlyText } from '../screen-reader-only-text/ScreenReaderOnlyText'; @@ -137,11 +137,11 @@ export const TextArea: VoidFunctionComponent = ({ ]); return ( - = ({ )} - + ); }; diff --git a/packages/react/src/components/text-input/styles/inputs.tsx b/packages/react/src/components/text-input/styles/inputs.tsx index a214ed7656..0d6eaf951b 100644 --- a/packages/react/src/components/text-input/styles/inputs.tsx +++ b/packages/react/src/components/text-input/styles/inputs.tsx @@ -1,7 +1,10 @@ -import { css, FlattenSimpleInterpolation } from 'styled-components'; +import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, ReactElement, Ref } from 'react'; +import styled, { css, FlattenSimpleInterpolation } from 'styled-components'; +import { useId } from '../../../hooks/use-id'; import { ResolvedTheme } from '../../../themes/theme'; import { focus } from '../../../utils/css-state'; -import { DeviceContextProps } from '../../device-context-provider/device-context-provider'; +import { DeviceContextProps, useDeviceContext } from '../../device-context-provider/device-context-provider'; +import { FormFieldControlProps, useFormFieldContext } from '../../form/form-field-context'; export const inputsStyle: (theme: ResolvedTheme, isMobile?: boolean) => FlattenSimpleInterpolation = ( theme: ResolvedTheme, @@ -77,3 +80,80 @@ export const responsiveInputsStyle = ({ theme, device: { isMobile } }: Responsiv ${focus({ theme }, true)} `; + +const StyledInput = styled.input<{ isMobile: boolean; }>` + ${({ theme, isMobile }) => inputsStyle(theme, isMobile)} +`; + +type PartialInputProps = Pick, HTMLInputElement>, + | 'id' + | 'className' + | 'type' + | 'name' + | 'value' + | 'defaultValue' + | 'placeholder' + | 'disabled' + | 'required' + | 'pattern' + | 'onBlur' + | 'onChange' + | 'onClick' + | 'onFocus' + | 'onKeyUp' + | 'onKeyDown' + | 'onMouseUp' + | 'onInvalid' + | 'inputMode' + | 'autoComplete' +>; + +type PartialFormFieldProps = Pick; + +export type InputProps = PartialInputProps & PartialFormFieldProps; + +export const Input = forwardRef(({ + id: providedId, + valid: providedValid = true, + required: providedRequired = false, + disabled: providedDisabled = false, + ...otherProps +}: InputProps, ref: Ref): ReactElement => { + const { isMobile } = useDeviceContext(); + const inputId = useId(providedId); + + const { + formId, + ariaLabel, + ariaLabelledby, + ariaDescribedby, + valid = providedValid, + required = providedRequired, + disabled = providedDisabled, + } = useFormFieldContext(otherProps); + + return ( + + ); +}); + +Input.displayName = 'Input'; diff --git a/packages/react/src/components/text-input/text-input.tsx b/packages/react/src/components/text-input/text-input.tsx index 96cacca054..71ecf8c196 100644 --- a/packages/react/src/components/text-input/text-input.tsx +++ b/packages/react/src/components/text-input/text-input.tsx @@ -17,7 +17,7 @@ import styled from 'styled-components'; import { useDataAttributes } from '../../hooks/use-data-attributes'; import { useTranslation } from '../../i18n/use-translation'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FieldContainer } from '../field-container/field-container'; +import { FormFieldContainer } from '../form/form-container/form-field-container'; import { TooltipProps } from '../tooltip/tooltip'; import { inputsStyle } from './styles/inputs'; import { useAriaConditionalIds } from '../../hooks/use-aria-conditional-ids'; @@ -138,10 +138,10 @@ export const TextInput = forwardRef(({ }, [valid]); return ( - - + ); }); From a62ebb42fe2d6ffd723a53da9f8b5e040cc7dbf6 Mon Sep 17 00:00:00 2001 From: Henri Bernard <132286421+hebernardEquisoft@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:16:45 -0400 Subject: [PATCH 02/23] chore(form-field): update snapshots --- .../form-field-container.test.tsx.snap | 124 ------------------ .../form-container/form-field-container.tsx | 4 - .../components/text-input/styles/inputs.tsx | 2 +- 3 files changed, 1 insertion(+), 129 deletions(-) diff --git a/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap index 8877ef1137..3b4f2540a5 100644 --- a/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap +++ b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap @@ -37,10 +37,6 @@ exports[`Form Field Container Styling matches default snapshot 1`] = ` border-color: #60666E; } -.c0:focus { - border-color: #006296; -} - .c0 > :nth-child(0) { margin-bottom: var(--spacing-half); } @@ -119,123 +115,3 @@ exports[`Form Field Container Styling matches invalid snapshot 1`] = ` Children `; - -exports[`Form Field Styling match noMargin snapshot 1`] = ` -.c0 { - margin: 0; -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #60666E; -} - -.c0:focus { - border-color: #006296; -} - -.c0 > :nth-child(0) { - margin-bottom: var(--spacing-half); -} - -
- Children -
-`; - -exports[`Form Field Styling matches default snapshot 1`] = ` -.c0 { - margin: 0 0 var(--spacing-3x); -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #60666E; -} - -.c0:focus { - border-color: #006296; -} - -.c0 > :nth-child(0) { - margin-bottom: var(--spacing-half); -} - -
- Children -
-`; - -exports[`Form Field Styling matches invalid snapshot 1`] = ` -.c1 { - color: #CD2C23; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - font-size: 0.75rem; - font-weight: var(--font-normal); - -webkit-letter-spacing: 0.02rem; - -moz-letter-spacing: 0.02rem; - -ms-letter-spacing: 0.02rem; - letter-spacing: 0.02rem; - line-height: 1.25rem; -} - -.c2 { - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - margin-right: var(--spacing-base); -} - -.c0 { - margin: 0 0 var(--spacing-3x); -} - -.c0 input, -.c0 select, -.c0 textarea { - border-color: #CD2C23; -} - -.c0:focus { - border-color: #CD2C23; -} - -.c0 > :nth-child(1) { - margin-bottom: var(--spacing-half); -} - -
- - - This text area input is invalid - - Children -
-`; diff --git a/packages/react/src/components/form/form-container/form-field-container.tsx b/packages/react/src/components/form/form-container/form-field-container.tsx index 0168406e72..3d4ef8bd18 100644 --- a/packages/react/src/components/form/form-container/form-field-container.tsx +++ b/packages/react/src/components/form/form-container/form-field-container.tsx @@ -26,10 +26,6 @@ const StyledDiv = styled.div` border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; } - &:focus { - border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-focus-border-color'] : theme.component['field-input-error-focus-border-color'])}; - } - > :nth-child(${({ hasLabel, hasHint, valid }) => (hasLabel ? 1 : 0) + (hasHint ? 1 : 0) + (!valid ? 1 : 0)}) { margin-bottom: var(--spacing-half); } diff --git a/packages/react/src/components/text-input/styles/inputs.tsx b/packages/react/src/components/text-input/styles/inputs.tsx index 7136d98656..fa9abd2b8c 100644 --- a/packages/react/src/components/text-input/styles/inputs.tsx +++ b/packages/react/src/components/text-input/styles/inputs.tsx @@ -91,7 +91,7 @@ export const responsiveInputsStyle = ({ theme, device: { isMobile } }: Responsiv `; const StyledInput = styled.input<{ isMobile: boolean; }>` - ${({ theme, isMobile }) => inputsStyle(theme, isMobile)} + ${({ theme, isMobile }) => inputsStyle({ theme, isMobile })} `; type PartialInputProps = Pick, HTMLInputElement>, From aac841df6536cc584b4ee6267bbe689c060efa77 Mon Sep 17 00:00:00 2001 From: Henri Bernard <132286421+hebernardEquisoft@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:25:20 -0400 Subject: [PATCH 03/23] chore(form-field): update snapshots --- .../form-container/form-field-container.test.tsx.snap | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap index 3b4f2540a5..c91fb3b3da 100644 --- a/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap +++ b/packages/react/src/components/form/form-container/form-field-container.test.tsx.snap @@ -11,10 +11,6 @@ exports[`Form Field Container Styling match noMargin snapshot 1`] = ` border-color: #60666E; } -.c0:focus { - border-color: #006296; -} - .c0 > :nth-child(0) { margin-bottom: var(--spacing-half); } @@ -85,10 +81,6 @@ exports[`Form Field Container Styling matches invalid snapshot 1`] = ` border-color: #CD2C23; } -.c0:focus { - border-color: #CD2C23; -} - .c0 > :nth-child(1) { margin-bottom: var(--spacing-half); } From cfb44ab1c400718abd17b2e6512d29e2d6857352 Mon Sep 17 00:00:00 2001 From: Henri Bernard <132286421+hebernardEquisoft@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:31:37 -0400 Subject: [PATCH 04/23] chore(form-field): post review changes --- .../form-field-container.test.tsx | 109 +++++++++++++++++- .../form-container/form-field-container.tsx | 84 +++++++------- .../src/components/form/form-field-context.ts | 31 ++--- packages/react/src/components/form/utils.ts | 39 +++---- .../components/text-input/styles/inputs.tsx | 12 +- packages/react/src/utils/string.test.ts | 16 ++- packages/react/src/utils/string.ts | 5 + 7 files changed, 194 insertions(+), 102 deletions(-) diff --git a/packages/react/src/components/form/form-container/form-field-container.test.tsx b/packages/react/src/components/form/form-container/form-field-container.test.tsx index cc0b982b23..abf9730a03 100644 --- a/packages/react/src/components/form/form-container/form-field-container.test.tsx +++ b/packages/react/src/components/form/form-container/form-field-container.test.tsx @@ -1,7 +1,15 @@ import { renderWithProviders } from '../../../test-utils/renderer'; import { devConsole } from '../../../utils/dev-console'; import { Input } from '../../text-input/styles/inputs'; -import { ARIA_LABEL_WARNING } from '../utils'; +import { + ARIA_LABEL_WARNING, + getAriaDescribedby, + getAriaLabel, + getAriaLabelledby, + getSlotId, + getSlotIds, + validateAriaLabels, +} from '../utils'; import { FormFieldContainer } from './form-field-container'; describe('Form Field Container', () => { @@ -23,7 +31,7 @@ describe('Form Field Container', () => { test('matches invalid snapshot', () => { const tree = renderWithProviders( - + Children , ); @@ -167,7 +175,7 @@ describe('Form Field Container', () => { consoleSpy.mockRestore(); }); - test('should contain idAdditionalElement when ariaLabelledBy provided', () => { + test('should contain idAdditionalElement when ariaLabelledby provided', () => { const consoleSpy = jest.spyOn(devConsole, 'warn'); consoleSpy.mockImplementation(() => {}); const wrapper = renderWithProviders( @@ -231,7 +239,7 @@ describe('Form Field Container', () => { @@ -277,7 +285,7 @@ describe('Form Field Container', () => { label={label} hint={hint} id={providedId} - valid={false} + invalid validationErrorMessage="" > @@ -315,4 +323,95 @@ describe('Form Field Container', () => { }); }); }); + + describe('Utils', () => { + describe('validateAriaLabels', () => { + it('returns true when only one label is provided', () => { + expect(validateAriaLabels('label')).toBe(true); + expect(validateAriaLabels(undefined, 'ariaLabel')).toBe(true); + expect(validateAriaLabels(undefined, undefined, 'ariaLabelledby')).toBe(true); + }); + + it('returns false when no labels are provided', () => { + expect(validateAriaLabels()).toBe(false); + }); + + it('returns false when more than one label is provided', () => { + expect(validateAriaLabels('label', 'ariaLabel')).toBe(false); + expect(validateAriaLabels('label', undefined, 'ariaLabelledby')).toBe(false); + expect(validateAriaLabels(undefined, 'ariaLabel', 'ariaLabelledby')).toBe(false); + }); + }); + + describe('getAriaLabel', () => { + it('returns ariaLabel when labels are valid', () => { + expect(getAriaLabel(undefined, 'ariaLabel', undefined)).toEqual('ariaLabel'); + }); + + it('returns undefined when labels are not valid', () => { + expect(getAriaLabel('label', 'ariaLabel', undefined)).toBeUndefined(); + }); + }); + + describe('getAriaLabelledby', () => { + it('joins label and additionalLabelledby with a space', () => { + const output = getAriaLabelledby({ label: 'label' }, 'additionalLabelledby'); + expect(output).toEqual('label additionalLabelledby'); + }); + + it('returns label when additionalLabelledby is undefined', () => { + expect(getAriaLabelledby({ label: 'label' })).toEqual('label'); + }); + + it('returns undefined when label and additionalLabelledby are undefined', () => { + expect(getAriaLabelledby({})).toBeUndefined(); + }); + }); + + describe('getAriaDescribedby', () => { + it('joins invalid, hint and additionalDescribedby with a space', () => { + const output = getAriaDescribedby({ invalid: 'invalid', hint: 'hint' }, 'additionalDescribedby'); + expect(output).toEqual('invalid hint additionalDescribedby'); + }); + + it('returns invalid and hint when additionalDescribedby is undefined', () => { + const output = getAriaDescribedby({ invalid: 'invalid', hint: 'hint' }); + expect(output).toEqual('invalid hint'); + }); + + it('returns undefined when invalid, hint and additionalDescribedby are undefined', () => { + expect(getAriaDescribedby({})).toBeUndefined(); + }); + }); + + describe('getSlotId', () => { + it('returns a string with formFieldId and propName when prop is provided', () => { + expect(getSlotId('prop', 'formFieldId', 'propName')).toEqual('formFieldId_propName'); + }); + + it('returns undefined when prop is not provided', () => { + expect(getSlotId(undefined, 'formFieldId', 'propName')).toBeUndefined(); + }); + }); + + describe('getSlotIds', () => { + it('returns an object with label, hint, and invalid ids when all props are provided', () => { + const result = getSlotIds('formFieldId', 'label', 'hint', 'invalid'); + expect(result).toEqual({ + label: 'formFieldId_label', + hint: 'formFieldId_hint', + invalid: 'formFieldId_invalid', + }); + }); + + it('returns an object with undefined ids when props are not provided', () => { + const result = getSlotIds('formFieldId'); + expect(result).toEqual({ + label: undefined, + hint: undefined, + invalid: undefined, + }); + }); + }); + }); }); diff --git a/packages/react/src/components/form/form-container/form-field-container.tsx b/packages/react/src/components/form/form-container/form-field-container.tsx index 3d4ef8bd18..19e623c0ec 100644 --- a/packages/react/src/components/form/form-container/form-field-container.tsx +++ b/packages/react/src/components/form/form-container/form-field-container.tsx @@ -1,32 +1,30 @@ import { FunctionComponent, PropsWithChildren, useMemo } from 'react'; import styled from 'styled-components'; import { useId } from '../../../hooks/use-id'; -import { ResolvedTheme } from '../../../themes/theme'; import { useDeviceContext } from '../../device-context-provider/device-context-provider'; import { InvalidField } from '../feedbacks/invalid-field'; import { Label } from '../../label/label'; import { TooltipProps } from '../../tooltip/tooltip'; import { FormFieldContext, FormFieldControlProps } from '../form-field-context'; -import { getAriaDescribedBy, getAriaLabel, getAriaLabelledBy, getSlotIds } from '../utils'; +import { getAriaDescribedby, getAriaLabel, getAriaLabelledby, getSlotIds } from '../utils'; interface StyledDivProps { - theme: ResolvedTheme; - hasLabel: boolean; - hasHint: boolean; - valid: boolean; - noMargin?: boolean; + $hasLabel: boolean; + $hasHint: boolean; + $invalid: boolean; + $noMargin?: boolean; } const StyledDiv = styled.div` - margin: ${({ noMargin }) => (noMargin ? '0' : '0 0 var(--spacing-3x)')}; + margin: ${({ $noMargin }) => ($noMargin ? '0' : '0 0 var(--spacing-3x)')}; input, select, textarea { - border-color: ${({ theme, valid }) => (valid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; + border-color: ${({ theme, $invalid }) => (!$invalid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; } - > :nth-child(${({ hasLabel, hasHint, valid }) => (hasLabel ? 1 : 0) + (hasHint ? 1 : 0) + (!valid ? 1 : 0)}) { + > :nth-child(${({ $hasLabel, $hasHint, $invalid }) => ($hasLabel ? 1 : 0) + ($hasHint ? 1 : 0) + ($invalid ? 1 : 0)}) { margin-bottom: var(--spacing-half); } `; @@ -65,64 +63,64 @@ export const FormFieldContainer: FunctionComponent { const { isMobile } = useDeviceContext(); const formId = useId(providedId); - const slotIds = getSlotIds(formId, label, hint, (validationErrorMessage && !valid)); + const slotIds = getSlotIds(formId, label, hint, (validationErrorMessage && invalid)); const ariaLabel = getAriaLabel(label, providedAriaLabel, providedAriaLabelledby); - const ariaLabelledby = getAriaLabelledBy(slotIds, providedAriaLabelledby); - const ariaDescribedby = getAriaDescribedBy(slotIds, providedAriaDescribedby); + const ariaLabelledby = getAriaLabelledby(slotIds, providedAriaLabelledby); + const ariaDescribedby = getAriaDescribedby(slotIds, providedAriaDescribedby); const contextValues = useMemo(() => ({ formId, ariaLabel, ariaLabelledby, ariaDescribedby, - valid, + invalid, hint, required, disabled, - }), [formId, ariaLabel, ariaLabelledby, ariaDescribedby, valid, hint, required, disabled]); + }), [formId, ariaLabel, ariaLabelledby, ariaDescribedby, invalid, hint, required, disabled]); return ( {label && ( - - )} + + )} {hint && ( - - {hint} - - )} - {!valid && ( - - )} + + {hint} + + )} + {invalid && ( + + )} {children} diff --git a/packages/react/src/components/form/form-field-context.ts b/packages/react/src/components/form/form-field-context.ts index d06906c66e..096e9a00c4 100644 --- a/packages/react/src/components/form/form-field-context.ts +++ b/packages/react/src/components/form/form-field-context.ts @@ -7,33 +7,16 @@ export interface FormFieldControlProps { ariaDescribedby?: string disabled?: boolean; required?: boolean; - valid?: boolean; + invalid?: boolean; } export const FormFieldContext = createContext({}); -function applyDefault(fields: T, defaults: T, keys: (keyof T)[]): T { - const result = {}; - keys.forEach((key) => { - result[key] = fields[key] === undefined ? defaults[key] : fields[key]; - }); - return result; -} - -export function useFormFieldContext(props: FormFieldControlProps): FormFieldControlProps { - const context = useContext(FormFieldContext); +export function useFormFieldContext(newContext: FormFieldControlProps): FormFieldControlProps { + const prevContext = useContext(FormFieldContext); - return applyDefault( - props, - context, - [ - 'formId', - 'ariaLabel', - 'ariaLabelledby', - 'ariaDescribedby', - 'valid', - 'required', - 'disabled', - ], - ); + return { + ...prevContext, + ...newContext, + }; } diff --git a/packages/react/src/components/form/utils.ts b/packages/react/src/components/form/utils.ts index 3fb12c1559..c1203970ab 100644 --- a/packages/react/src/components/form/utils.ts +++ b/packages/react/src/components/form/utils.ts @@ -1,4 +1,6 @@ +import { ReactNode } from 'react'; import { devConsole } from '../../utils/dev-console'; +import { joinStrings } from '../../utils/string'; interface FormFieldIds { label?: string; @@ -6,24 +8,20 @@ interface FormFieldIds { invalid?: string; } -function makeSlotId(prop: React.ReactNode, formFieldId: string, propName: string): string | undefined { - if (!prop) { - return undefined; - } - - return `${formFieldId}_${propName}`; +export function getSlotId(prop: ReactNode, formFieldId: string, propName: string): string | undefined { + return prop ? `${formFieldId}_${propName}` : undefined; } export function getSlotIds( formFieldId: string, - label?: React.ReactNode, - hint?: React.ReactNode, - invalid?: React.ReactNode, + label?: ReactNode, + hint?: ReactNode, + invalid?: ReactNode, ): FormFieldIds { return { - label: makeSlotId(label, formFieldId, 'label'), - hint: makeSlotId(hint, formFieldId, 'hint'), - invalid: makeSlotId(invalid, formFieldId, 'invalid'), + label: getSlotId(label, formFieldId, 'label'), + hint: getSlotId(hint, formFieldId, 'hint'), + invalid: getSlotId(invalid, formFieldId, 'invalid'), }; } @@ -43,11 +41,6 @@ export const validateAriaLabels = ( return true; }; -export function joinStrings(...strings: Array): string | undefined { - const filteredStrings = strings.filter(Boolean); - return filteredStrings.length > 0 ? filteredStrings.join(' ') : undefined; -} - export function getAriaLabel( label?: string, ariaLabel?: string, @@ -56,16 +49,16 @@ export function getAriaLabel( return validateAriaLabels(label, ariaLabel, ariaLabelledby) ? ariaLabel : undefined; } -export function getAriaLabelledBy( +export function getAriaLabelledby( { label }: FormFieldIds, - additionalLabelledBy?: string, + additionalLabelledby?: string, ): string | undefined { - return joinStrings(label, additionalLabelledBy); + return joinStrings(label, additionalLabelledby); } -export function getAriaDescribedBy( +export function getAriaDescribedby( { invalid, hint }: FormFieldIds, - additionalDescribedBy?: string, + additionalDescribedby?: string, ): string | undefined { - return joinStrings(invalid, hint, additionalDescribedBy); + return joinStrings(invalid, hint, additionalDescribedby); } diff --git a/packages/react/src/components/text-input/styles/inputs.tsx b/packages/react/src/components/text-input/styles/inputs.tsx index fa9abd2b8c..601caa7d0c 100644 --- a/packages/react/src/components/text-input/styles/inputs.tsx +++ b/packages/react/src/components/text-input/styles/inputs.tsx @@ -121,14 +121,14 @@ type PartialFormFieldProps = Pick; export type InputProps = PartialInputProps & PartialFormFieldProps; export const Input = forwardRef(({ id: providedId, - valid: providedValid = true, + invalid: providedInvalid = true, required: providedRequired = false, disabled: providedDisabled = false, ...otherProps @@ -141,7 +141,7 @@ export const Input = forwardRef(({ ariaLabel, ariaLabelledby, ariaDescribedby, - valid = providedValid, + invalid = providedInvalid, required = providedRequired, disabled = providedDisabled, } = useFormFieldContext(otherProps); @@ -151,9 +151,9 @@ export const Input = forwardRef(({ aria-label={ariaLabel} aria-labelledby={ariaLabel ? undefined : ariaLabelledby} aria-describedby={ariaDescribedby} - aria-disabled={disabled ? 'true' : 'false'} - aria-required={required ? 'true' : 'false'} - aria-invalid={valid ? 'false' : 'true'} + aria-disabled={disabled} + aria-required={required} + aria-invalid={invalid} data-testid="input" id={formId || inputId} disabled={disabled} diff --git a/packages/react/src/utils/string.test.ts b/packages/react/src/utils/string.test.ts index 80ee7b0fa6..e0fd63004a 100644 --- a/packages/react/src/utils/string.test.ts +++ b/packages/react/src/utils/string.test.ts @@ -1,4 +1,4 @@ -import { allSameLetter, stripDiacritics } from './string'; +import { allSameLetter, joinStrings, stripDiacritics } from './string'; describe('string utilities', () => { describe('allSameLetter', () => { @@ -16,4 +16,18 @@ describe('string utilities', () => { expect(stripDiacritics('Où il est le Père Noël, déjà?')).toBe('Ou il est le Pere Noel, deja?'); }); }); + + describe('joinStrings', () => { + it('joins multiple strings with a space', () => { + expect(joinStrings('a', 'b', 'c')).toEqual('a b c'); + }); + + it('ignores undefined values', () => { + expect(joinStrings('a', undefined, 'c')).toEqual('a c'); + }); + + it('returns undefined when all values are undefined', () => { + expect(joinStrings(undefined, undefined, undefined)).toBeUndefined(); + }); + }); }); diff --git a/packages/react/src/utils/string.ts b/packages/react/src/utils/string.ts index 288ee749b5..438a258c44 100644 --- a/packages/react/src/utils/string.ts +++ b/packages/react/src/utils/string.ts @@ -7,3 +7,8 @@ export function allSameLetter(text: string): boolean { export function stripDiacritics(input: string): string { return input.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } + +export function joinStrings(...strings: Array): string | undefined { + const filteredStrings = strings.filter(Boolean); + return filteredStrings.length > 0 ? filteredStrings.join(' ') : undefined; +} From 34c1a19a0b06336dcabbb9304955258a0cf627ea Mon Sep 17 00:00:00 2001 From: Henri Bernard <132286421+hebernardEquisoft@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:00:38 -0400 Subject: [PATCH 05/23] chore(form-field): post review changes test fixes --- .../react/src/components/combobox/combobox.tsx | 4 ++-- .../src/components/date-picker/date-picker.tsx | 2 +- .../components/dropdown-list/dropdown-list.tsx | 2 +- .../components/numeric-input/numeric-input.tsx | 2 +- .../password-creation-input.tsx | 2 +- .../components/password-input/password-input.tsx | 2 +- .../components/stepper-input/stepper-input.tsx | 4 ++-- .../src/components/text-area/text-area.test.tsx | 2 +- .../react/src/components/text-area/text-area.tsx | 2 +- .../components/text-input/text-input.test.tsx | 16 ++++++++-------- .../src/components/text-input/text-input.tsx | 2 +- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/react/src/components/combobox/combobox.tsx b/packages/react/src/components/combobox/combobox.tsx index 3f4a1b8f37..cbd14a3ba7 100644 --- a/packages/react/src/components/combobox/combobox.tsx +++ b/packages/react/src/components/combobox/combobox.tsx @@ -76,7 +76,7 @@ const Textbox = styled.input` width: 100%; ${focus}; - + &::placeholder { color: ${({ theme }) => theme.component['combobox-placeholder-text-color']}; font-style: italic; @@ -597,7 +597,7 @@ export const Combobox: VoidFunctionComponent = ({ label={label} required={required} tooltip={tooltip} - valid={valid} + invalid={!valid} validationErrorMessage={validationErrorMessage || t('validationErrorMessage')} hint={hint} > diff --git a/packages/react/src/components/date-picker/date-picker.tsx b/packages/react/src/components/date-picker/date-picker.tsx index 1012c6d14c..6b642c870c 100644 --- a/packages/react/src/components/date-picker/date-picker.tsx +++ b/packages/react/src/components/date-picker/date-picker.tsx @@ -517,7 +517,7 @@ export const Datepicker = forwardRef(({ required={required} tooltip={tooltip} hint={hint} - valid={valid} + invalid={!valid} validationErrorMessage={validationErrorMessage || t('validationErrorMessage')} > diff --git a/packages/react/src/components/dropdown-list/dropdown-list.tsx b/packages/react/src/components/dropdown-list/dropdown-list.tsx index cd85104654..bff24a386a 100644 --- a/packages/react/src/components/dropdown-list/dropdown-list.tsx +++ b/packages/react/src/components/dropdown-list/dropdown-list.tsx @@ -479,7 +479,7 @@ export const DropdownList: VoidFunctionComponent diff --git a/packages/react/src/components/numeric-input/numeric-input.tsx b/packages/react/src/components/numeric-input/numeric-input.tsx index 25cb472050..5fb36329d6 100644 --- a/packages/react/src/components/numeric-input/numeric-input.tsx +++ b/packages/react/src/components/numeric-input/numeric-input.tsx @@ -148,7 +148,7 @@ export const NumericInput: VoidFunctionComponent = ({ label={label} tooltip={tooltip} noMargin={noMargin} - valid={!numericInput.invalid} + invalid={numericInput.invalid} noInvalidFieldIcon={!numericInput.validationErrorMessage} validationErrorMessage={numericInput.validationErrorMessage ?? ''} > diff --git a/packages/react/src/components/password-creation-input/password-creation-input.tsx b/packages/react/src/components/password-creation-input/password-creation-input.tsx index 7fc2d9ccd5..53e28ad3af 100644 --- a/packages/react/src/components/password-creation-input/password-creation-input.tsx +++ b/packages/react/src/components/password-creation-input/password-creation-input.tsx @@ -78,7 +78,7 @@ export const PasswordCreationInput: VoidFunctionComponent `; -exports[`Form Field Container Styling matches default snapshot 1`] = ` +exports[`Field Container Styling matches default snapshot 1`] = ` .c0 { margin: 0 0 var(--spacing-3x); } @@ -44,7 +44,7 @@ exports[`Form Field Container Styling matches default snapshot 1`] = ` `; -exports[`Form Field Container Styling matches invalid snapshot 1`] = ` +exports[`Field Container Styling matches invalid snapshot 1`] = ` .c1 { color: #CD2C23; display: -webkit-box; diff --git a/packages/react/src/components/form/form-container/form-field-container.tsx b/packages/react/src/components/field/field-container.tsx similarity index 61% rename from packages/react/src/components/form/form-container/form-field-container.tsx rename to packages/react/src/components/field/field-container.tsx index e2d61d41a4..7ae74fd421 100644 --- a/packages/react/src/components/form/form-container/form-field-container.tsx +++ b/packages/react/src/components/field/field-container.tsx @@ -1,17 +1,17 @@ import { FunctionComponent, PropsWithChildren, useMemo } from 'react'; import styled from 'styled-components'; -import { useId } from '../../../hooks/use-id'; -import { useDeviceContext } from '../../device-context-provider/device-context-provider'; -import { InvalidField } from '../feedbacks/invalid-field'; -import { Label } from '../../label/label'; -import { TooltipProps } from '../../tooltip/tooltip'; -import { FormFieldContext, FormFieldControlProps } from '../form-field-context'; -import { getAriaDescribedby, getAriaLabel, getAriaLabelledby, getSlotIds } from '../utils'; +import { useId } from '../../hooks/use-id'; +import { useDeviceContext } from '../device-context-provider/device-context-provider'; +import { FieldControlContext, FieldControlProps } from './context'; +import { InvalidFieldMessage } from './feedbacks/invalid-field-message'; +import { Label } from '../label/label'; +import { TooltipProps } from '../tooltip/tooltip'; +import { getAriaDescribedby, getAriaLabel, getAriaLabelledby, getSlotIds } from './utils'; interface StyledDivProps { $hasLabel: boolean; $hasHint: boolean; - $invalid: boolean; + $valid: boolean; $noMargin?: boolean; } @@ -21,35 +21,34 @@ const StyledDiv = styled.div` input, select, textarea { - border-color: ${({ theme, $invalid }) => ($invalid ? theme.component['field-input-error-border-color'] : theme.component['field-input-border-color'])}; + border-color: ${({ theme, $valid }) => ($valid ? theme.component['field-input-border-color'] : theme.component['field-input-error-border-color'])}; } - > :nth-child(${({ $hasLabel, $hasHint, $invalid }) => ($hasLabel ? 1 : 0) + ($hasHint ? 1 : 0) + ($invalid ? 1 : 0)}) { + > :nth-child(${({ $hasLabel, $hasHint, $valid }) => ($hasLabel ? 1 : 0) + ($hasHint ? 1 : 0) + (!$valid ? 1 : 0)}) { margin-bottom: var(--spacing-half); } `; -const StyledHint = styled.span<{ isMobile: boolean }>` +const StyledHint = styled.span<{ $isMobile: boolean }>` color: ${(props) => props.theme.component['field-hint-text-color']}; display: block; - font-size: ${({ isMobile }) => (isMobile ? '0.875rem' : '0.75rem')}; + font-size: ${({ $isMobile }) => ($isMobile ? '0.875rem' : '0.75rem')}; font-weight: var(--font-normal); letter-spacing: 0.02rem; - line-height: ${({ isMobile }) => (isMobile ? '1.5rem' : '1.25rem')}; + line-height: ${({ $isMobile }) => ($isMobile ? '1.5rem' : '1.25rem')}; `; -export interface FormFieldContainerProps extends FormFieldControlProps { +export interface FieldContainerProps extends FieldControlProps { className?: string; - id?: string; - noInvalidFieldIcon?: boolean; noMargin?: boolean; tooltip?: TooltipProps; label?: string; hint?: string; + noInvalidFieldIcon?: boolean; validationErrorMessage: string; } -export const FormFieldContainer: FunctionComponent> = ({ +export const FieldContainer: FunctionComponent> = ({ id: providedId, ariaLabel: providedAriaLabel, ariaLabelledby: providedAriaLabelledby, @@ -59,47 +58,47 @@ export const FormFieldContainer: FunctionComponent { const { isMobile } = useDeviceContext(); - const formId = useId(providedId); + const fieldId = useId(providedId); - const slotIds = getSlotIds(formId, label, hint, (validationErrorMessage && invalid)); + const slotIds = getSlotIds(fieldId, label, hint, (validationErrorMessage && !valid)); const ariaLabel = getAriaLabel(label, providedAriaLabel, providedAriaLabelledby); const ariaLabelledby = getAriaLabelledby(slotIds, providedAriaLabelledby); const ariaDescribedby = getAriaDescribedby(slotIds, providedAriaDescribedby); const contextValues = useMemo(() => ({ - formId, + id: fieldId, ariaLabel, ariaLabelledby, ariaDescribedby, - invalid, + valid, hint, required, disabled, - }), [formId, ariaLabel, ariaLabelledby, ariaDescribedby, invalid, hint, required, disabled]); + }), [fieldId, ariaLabel, ariaLabelledby, ariaDescribedby, valid, hint, required, disabled]); return ( - + {label && ( + ); }; diff --git a/packages/react/src/components/form/utils.ts b/packages/react/src/components/field/utils.ts similarity index 94% rename from packages/react/src/components/form/utils.ts rename to packages/react/src/components/field/utils.ts index c1203970ab..0bdb4cf769 100644 --- a/packages/react/src/components/form/utils.ts +++ b/packages/react/src/components/field/utils.ts @@ -2,7 +2,7 @@ import { ReactNode } from 'react'; import { devConsole } from '../../utils/dev-console'; import { joinStrings } from '../../utils/string'; -interface FormFieldIds { +interface FieldIds { label?: string; hint?: string; invalid?: string; @@ -17,7 +17,7 @@ export function getSlotIds( label?: ReactNode, hint?: ReactNode, invalid?: ReactNode, -): FormFieldIds { +): FieldIds { return { label: getSlotId(label, formFieldId, 'label'), hint: getSlotId(hint, formFieldId, 'hint'), @@ -50,14 +50,14 @@ export function getAriaLabel( } export function getAriaLabelledby( - { label }: FormFieldIds, + { label }: FieldIds, additionalLabelledby?: string, ): string | undefined { return joinStrings(label, additionalLabelledby); } export function getAriaDescribedby( - { invalid, hint }: FormFieldIds, + { invalid, hint }: FieldIds, additionalDescribedby?: string, ): string | undefined { return joinStrings(invalid, hint, additionalDescribedby); diff --git a/packages/react/src/components/form/form-field-context.ts b/packages/react/src/components/form/form-field-context.ts deleted file mode 100644 index 096e9a00c4..0000000000 --- a/packages/react/src/components/form/form-field-context.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createContext, useContext } from 'react'; - -export interface FormFieldControlProps { - formId?: string; - ariaLabel?: string; - ariaLabelledby?: string; - ariaDescribedby?: string - disabled?: boolean; - required?: boolean; - invalid?: boolean; -} - -export const FormFieldContext = createContext({}); - -export function useFormFieldContext(newContext: FormFieldControlProps): FormFieldControlProps { - const prevContext = useContext(FormFieldContext); - - return { - ...prevContext, - ...newContext, - }; -} diff --git a/packages/react/src/components/numeric-input/numeric-input.tsx b/packages/react/src/components/numeric-input/numeric-input.tsx index 5fb36329d6..ec2cd83eb2 100644 --- a/packages/react/src/components/numeric-input/numeric-input.tsx +++ b/packages/react/src/components/numeric-input/numeric-input.tsx @@ -10,7 +10,7 @@ import { useId } from '../../hooks/use-id'; import { focus } from '../../utils/css-state'; import { ResolvedTheme } from '../../themes/theme'; import { DeviceContextProps, useDeviceContext } from '../device-context-provider/device-context-provider'; -import { FormFieldContainer } from '../form/form-container/form-field-container'; +import { FieldContainer } from '../field/field-container'; import { inputsStyle } from '../text-input/styles/inputs'; import { TooltipProps } from '../tooltip/tooltip'; import { useNumericInput, UseNumericInputParams } from './use-numeric-input'; @@ -141,14 +141,14 @@ export const NumericInput: VoidFunctionComponent = ({ ), [adornment, adornmentPosition]); return ( - @@ -172,6 +172,6 @@ export const NumericInput: VoidFunctionComponent = ({ /> {(adornment && adornmentPosition === 'end') && adornmentContent} - + ); }; diff --git a/packages/react/src/components/password-creation-input/password-creation-input.tsx b/packages/react/src/components/password-creation-input/password-creation-input.tsx index 53e28ad3af..2dfd4ed3a1 100644 --- a/packages/react/src/components/password-creation-input/password-creation-input.tsx +++ b/packages/react/src/components/password-creation-input/password-creation-input.tsx @@ -2,7 +2,7 @@ import { ChangeEvent, useState, VoidFunctionComponent, useMemo } from 'react'; import styled from 'styled-components'; import { useDeviceContext } from '../device-context-provider/device-context-provider'; import { IconButton } from '../buttons/icon-button'; -import { FormFieldContainer } from '../form/form-container/form-field-container'; +import { FieldContainer } from '../field/field-container'; import { Input } from '../text-input/text-input'; import { useTranslation } from '../../i18n/use-translation'; import { Tooltip } from '../tooltip/tooltip'; @@ -73,12 +73,12 @@ export const PasswordCreationInput: VoidFunctionComponent