diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/form-label/properties.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/form-label/properties.mdx index 322546e7f3e..929947fadae 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/form-label/properties.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/form-label/properties.mdx @@ -6,11 +6,12 @@ showTabs: true | Properties | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `for_id` | _(required)_ the `id` of the input. | -| `vertical` | _(optional)_ if set to `true`, will do the same as `label_direction` when set to **vertical**. | -| `title` | _(optional)_ the `title` attribute of the label. | +| `forId` | _(required)_ the `id` of the input. | | `text` | _(optional)_ the `text` of the label. | +| `srOnly` | _(optional)_ the `id` of the input. | +| `vertical` | _(optional)_ if set to `true`, will do the same as `label_direction` when set to **vertical**. | | `size` | _(optional)_ define one of the following [heading size](/uilib/elements/heading/): `medium` or `large`. | | `skeleton` | _(optional)_ if set to `true`, an overlaying skeleton with animation will be shown. | | `element` | _(optional)_ defines the HTML element used. Defaults to `label`. | +| `innerRef` | _(optional)_ attach a React Ref to the inner label `element`. | | [Space](/uilib/layout/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. | diff --git a/packages/dnb-eufemia/src/components/form-label/FormLabel.d.ts b/packages/dnb-eufemia/src/components/form-label/FormLabel.d.ts deleted file mode 100644 index 38286e54784..00000000000 --- a/packages/dnb-eufemia/src/components/form-label/FormLabel.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; -import type { SkeletonShow } from '../Skeleton'; -import type { SpacingProps } from '../space/types'; -export type FormLabelSize = 'medium' | 'large'; -export type FormLabelText = - | string - | ((...args: any[]) => any) - | React.ReactNode; -export type FormLabelLabelDirection = 'vertical' | 'horizontal'; -export type FormLabelChildren = - | string - | ((...args: any[]) => any) - | React.ReactNode; -export interface FormLabelProps - extends Omit, 'ref'>, - SpacingProps { - /** - * (required) the `id` of the input. - */ - for_id?: string; - /** - * Defines the HTML element used. Defaults to `label`. - */ - element?: string; - /** - * The `title` attribute of the label. - */ - title?: string; - /** - * The `text` of the label. - */ - text?: FormLabelText; - /** - * Define one of the following heading size: `medium` or `large`. - */ - size?: FormLabelSize; - id?: string; - class?: string; - disabled?: boolean; - /** - * If set to `true`, an overlaying skeleton with animation will be shown. - */ - skeleton?: SkeletonShow; - label_direction?: FormLabelLabelDirection; - /** - * If set to `true`, will do the same as `label_direction` when set to "vertical". - */ - vertical?: boolean; - sr_only?: boolean; - className?: string; - children?: FormLabelChildren; -} -export default class FormLabel extends React.Component< - FormLabelProps, - any -> { - static defaultProps: object; - render(): JSX.Element; -} diff --git a/packages/dnb-eufemia/src/components/form-label/FormLabel.js b/packages/dnb-eufemia/src/components/form-label/FormLabel.js deleted file mode 100644 index 92d8536e7df..00000000000 --- a/packages/dnb-eufemia/src/components/form-label/FormLabel.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Web FormLabel Component - * - */ - -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import { - extendPropsWithContextInClassComponent, - isTrue, - validateDOMAttributes, - processChildren, -} from '../../shared/component-helper' -import { - spacingPropTypes, - createSpacingClasses, -} from '../space/SpacingHelper' -import { - createSkeletonClass, - skeletonDOMAttributes, -} from '../skeleton/SkeletonHelper' -import { pickFormElementProps } from '../../shared/helpers/filterValidProps' -import Context from '../../shared/Context' - -export default class FormLabel extends React.PureComponent { - static contextType = Context - - static propTypes = { - for_id: PropTypes.string, - element: PropTypes.string, - title: PropTypes.string, - text: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - PropTypes.node, - ]), - size: PropTypes.oneOf(['basis', 'medium', 'large']), - id: PropTypes.string, - class: PropTypes.string, - disabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - label_direction: PropTypes.oneOf(['vertical', 'horizontal']), - vertical: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - sr_only: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - - ...spacingPropTypes, - - className: PropTypes.string, - children: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func, - PropTypes.node, - ]), - } - - static defaultProps = { - for_id: null, - element: 'label', - title: null, - text: null, - size: null, - id: null, - class: null, - disabled: null, - skeleton: null, - label_direction: null, - vertical: null, - sr_only: null, - - className: null, - children: null, - } - - static getContent(props) { - if (props.text) return props.text - return processChildren(props) - } - - render() { - // use only the props from context, who are available here anyway - const props = extendPropsWithContextInClassComponent( - this.props, - FormLabel.defaultProps, - { skeleton: this.context?.skeleton }, - // Deprecated – can be removed in v11 - pickFormElementProps(this.context?.FormRow), - pickFormElementProps(this.context?.formElement), - this.context.FormLabel - ) - - const { - for_id, - element, - title, - className, - id, - disabled, - skeleton, - label_direction, - vertical, - sr_only, - class: _className, - text: _text, // eslint-disable-line - size, - - ...attributes - } = props - - const content = FormLabel.getContent(this.props) - - const params = { - className: classnames( - 'dnb-form-label', - (isTrue(vertical) || label_direction === 'vertical') && - `dnb-form-label--vertical`, - isTrue(sr_only) && 'dnb-sr-only', - size && `dnb-h--${size}`, - createSkeletonClass('font', skeleton, this.context), - createSpacingClasses(props), - className, - _className - ), - htmlFor: for_id, - id, - title, - disabled: isTrue(disabled), - ...attributes, - } - - if (disabled) { - params.disabled = true - } - - skeletonDOMAttributes(params, skeleton, this.context) - - // also used for code markup simulation - validateDOMAttributes(this.props, params) - - params.children = content - - const Element = element - - // Use the font-swap feature dnb-skeleton--font - // if (isTrue(skeleton)) { - // // skeletonDOMAttributes(attributes, skeleton, this.context) - // return - // } - - return - } -} - -FormLabel._supportsSpacingProps = true diff --git a/packages/dnb-eufemia/src/components/form-label/FormLabel.tsx b/packages/dnb-eufemia/src/components/form-label/FormLabel.tsx new file mode 100644 index 00000000000..81916f44558 --- /dev/null +++ b/packages/dnb-eufemia/src/components/form-label/FormLabel.tsx @@ -0,0 +1,106 @@ +/** + * Web FormLabel Component + * + */ + +import React from 'react' +import classnames from 'classnames' +import { + extendPropsWithContext, + isTrue, + validateDOMAttributes, +} from '../../shared/component-helper' +import { createSpacingClasses } from '../space/SpacingHelper' +import { + createSkeletonClass, + skeletonDOMAttributes, +} from '../skeleton/SkeletonHelper' +import { pickFormElementProps } from '../../shared/helpers/filterValidProps' +import Context from '../../shared/Context' +import { + DynamicElement, + DynamicElementParams, + SpacingProps, +} from '../../shared/types' + +export type FormLabelProps = { + forId?: string + element?: DynamicElement + text?: React.ReactNode + size?: 'basis' | 'medium' | 'large' + id?: string + skeleton?: boolean + label?: React.ReactNode + vertical?: boolean + srOnly?: boolean + innerRef?: React.RefObject + + /** @deprecated use forId instead */ + for_id?: string + /** @deprecated use srOnly instead */ + sr_only?: boolean + /** @deprecated use labelDirection instead (was not documented before) */ + label_direction?: 'vertical' | 'horizontal' +} + +export type FormLabelAllProps = FormLabelProps & + React.HTMLAttributes & + SpacingProps + +export default function FormLabel(localProps: FormLabelAllProps) { + const context = React.useContext(Context) + + // use only the props from context, who are available here anyway + const props = extendPropsWithContext( + localProps, + null, + { skeleton: context?.skeleton }, + pickFormElementProps(context?.FormRow), // Deprecated – can be removed in v11 + pickFormElementProps(context?.formElement), + context?.FormLabel + ) + + const { + forId, + text, + srOnly, + vertical, + size, + skeleton, + element: Element = 'label', + innerRef, + className, + children, + + /** @deprecated can be removed in v11 */ + for_id, + sr_only, + label_direction, + + ...attributes + } = props + + const params = { + className: classnames( + 'dnb-form-label', + (isTrue(vertical) || label_direction === 'vertical') && + `dnb-form-label--vertical`, + (srOnly || isTrue(sr_only)) && 'dnb-sr-only', + size && `dnb-h--${size}`, + createSkeletonClass('font', skeleton, context), + createSpacingClasses(props), + className + ), + htmlFor: forId || for_id, + ...(attributes as DynamicElementParams), + } + + params['ref'] = innerRef + + skeletonDOMAttributes(params, skeleton, context) + validateDOMAttributes(localProps, params) + + return {text || children} +} + +FormLabel._supportsSpacingProps = true diff --git a/packages/dnb-eufemia/src/components/form-label/__tests__/FormLabel.test.tsx b/packages/dnb-eufemia/src/components/form-label/__tests__/FormLabel.test.tsx index 08bb2537202..a4f309af446 100644 --- a/packages/dnb-eufemia/src/components/form-label/__tests__/FormLabel.test.tsx +++ b/packages/dnb-eufemia/src/components/form-label/__tests__/FormLabel.test.tsx @@ -6,17 +6,13 @@ import React from 'react' import { axeComponent, loadScss } from '../../../core/jest/jestSetup' import { render } from '@testing-library/react' -import FormLabel, { FormLabelProps } from '../FormLabel' +import FormLabel from '../FormLabel' import Input from '../../input/Input' import { Provider } from '../../../shared' -const props: FormLabelProps = { - title: 'title', -} - describe('FormLabel component', () => { it('should forward unlisted attributes like "aria-hidden"', () => { - render() + render() expect( document.querySelector('label[aria-hidden]') ).toBeInTheDocument() @@ -26,7 +22,7 @@ describe('FormLabel component', () => { }) it('should support spacing props', () => { - render() + render() const element = document.querySelector('.dnb-form-label') @@ -36,8 +32,8 @@ describe('FormLabel component', () => { ]) }) - it('should set correct class when sr_only is set', () => { - render() + it('should set correct class when srOnly is set', () => { + render() const element = document.querySelector('.dnb-form-label') @@ -47,10 +43,18 @@ describe('FormLabel component', () => { ]) }) + it('should set correct for id', () => { + render() + + const element = document.querySelector('.dnb-form-label') + + expect(element.getAttribute('for')).toBe('unique-id') + }) + it('should inherit formElement vertical label', () => { render( - + ) @@ -59,7 +63,7 @@ describe('FormLabel component', () => { (attr) => attr.name ) - expect(attributes).toEqual(['class', 'label']) + expect(attributes).toEqual(['class']) expect(Array.from(element.classList)).toEqual([ 'dnb-form-label', 'dnb-form-label--vertical', @@ -68,35 +72,55 @@ describe('FormLabel component', () => { it('should support heading size prop', () => { const { rerender } = render( - - content - + content ) expect(document.querySelector('.dnb-form-label').classList).toContain( 'dnb-h--medium' ) - rerender( - - content - - ) + rerender(content) expect(document.querySelector('.dnb-form-label').classList).toContain( 'dnb-h--large' ) }) + it('should use label element by default', () => { + render(content) + + expect(document.querySelector('.dnb-form-label').tagName).toBe('LABEL') + }) + + it('gets valid ref element', () => { + let ref: React.RefObject + + function MockComponent() { + ref = React.useRef() + return content + } + + render() + + expect(ref.current instanceof HTMLLabelElement).toBe(true) + expect(ref.current.tagName).toBe('LABEL') + }) + it('should validate with ARIA rules', async () => { - const Comp = render() + const Comp = render( + + ) expect(await axeComponent(Comp)).toHaveNoViolations() }) it('should validate with ARIA rules as a label with a input', async () => { - const LabelComp = render() - const InputComp = render() - expect(await axeComponent(LabelComp, InputComp)).toHaveNoViolations() + const Comp = render( + <> + + + + ) + expect(await axeComponent(Comp)).toHaveNoViolations() }) }) diff --git a/packages/dnb-eufemia/src/components/space/Space.tsx b/packages/dnb-eufemia/src/components/space/Space.tsx index a61bf2b2182..edd43ddced4 100644 --- a/packages/dnb-eufemia/src/components/space/Space.tsx +++ b/packages/dnb-eufemia/src/components/space/Space.tsx @@ -22,7 +22,11 @@ import { createSkeletonClass, } from '../skeleton/SkeletonHelper' -import type { DynamicElement, SpacingProps } from '../../shared/types' +import type { + DynamicElement, + DynamicElementParams, + SpacingProps, +} from '../../shared/types' import type { SkeletonShow } from '../Skeleton' export { spacingPropTypes } @@ -145,7 +149,7 @@ function Element({ innerRef, ...props }: SpaceAllProps) { - const ElementDynamic = element as DynamicElement + const ElementDynamic = element if (element?.['_name'] === 'Section') { props['inner_ref'] = innerRef @@ -155,7 +159,11 @@ function Element({ props['ref'] = innerRef } - const component = {children} + const component = ( + + {children} + + ) if (isTrue(no_collapse)) { const R = diff --git a/packages/dnb-eufemia/src/shared/Context.tsx b/packages/dnb-eufemia/src/shared/Context.tsx index 0df3aa98802..ad967ebbbc1 100644 --- a/packages/dnb-eufemia/src/shared/Context.tsx +++ b/packages/dnb-eufemia/src/shared/Context.tsx @@ -39,6 +39,7 @@ import type { GlobalErrorProps } from '../components/GlobalError' import type { ModalProps } from '../components/modal/types' import type { AccordionProps } from '../components/Accordion' import type { StepIndicatorProps } from '../components/StepIndicator' +import type { FormLabelProps } from '../components/FormLabel' import type { NumberFormatCurrency } from '../components/NumberFormat' @@ -73,6 +74,7 @@ export type ContextComponents = { Modal?: Partial Accordion?: Partial StepIndicator?: Partial + FormLabel?: Partial // -- TODO: Not converted yet -- NumberFormat?: Record diff --git a/packages/dnb-eufemia/src/shared/types.tsx b/packages/dnb-eufemia/src/shared/types.tsx index d008b4fa8e8..bad2d304e24 100644 --- a/packages/dnb-eufemia/src/shared/types.tsx +++ b/packages/dnb-eufemia/src/shared/types.tsx @@ -24,9 +24,10 @@ export type DataAttributeTypes = { // [property: `data-${string}`]: string } -export type DynamicElement< - P = React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - >, -> = keyof JSX.IntrinsicElements | React.FunctionComponent

+export type DynamicElement = + | keyof JSX.IntrinsicElements + | React.FunctionComponent< + React.DetailedHTMLProps, E> + > + +export type DynamicElementParams> = T