diff --git a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx index 4291c976482..eb9084ad8a6 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx +++ b/packages/dnb-design-system-portal/src/docs/uilib/about-the-lib/releases/eufemia/v11-info.mdx @@ -52,6 +52,11 @@ For more information on how to replace these, check out [these docs](/uilib/layo - Find `FormRow:` and replace with `formElement:`. - Find `import { includeValidProps } from '@dnb/eufemia/components/form-row/FormRowHelpers'` and replace with `import { pickFormElementProps } from '@dnb/eufemia/shared/helpers/filterValidProps'`. +## FormLabel + +- Find `for_id` and replace with `forId`. +- Find `sr_only` and replace with `srOnly`. + ## Removal of passing down props to BreadcrumbItem's span We don't think this has been used for anything other than passing down `data-testid`'s for testing. We believe the potential side effects of passing down props to this span is greater than the advantages it gives for those who want to test this span using data-testid as their way of selecting the span. 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..faf150697ee 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. | +| `forId` | _(required)_ the same unique `id` like the linked HTML element has. | +| `text` | _(optional)_ the `text` of the label. You can use `children` as well. | +| `srOnly` | _(optional)_ when `true`, the label will be invisible and only accessible for screen readers. | | `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. | -| `text` | _(optional)_ the `text` of the label. | | `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..04027b0a416 --- /dev/null +++ b/packages/dnb-eufemia/src/components/form-label/FormLabel.tsx @@ -0,0 +1,109 @@ +/** + * 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 + + /** Is not a part of HTMLLabelElement and not documented as of now */ + disabled?: boolean + + /** @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/components/tabs/Tabs.d.ts b/packages/dnb-eufemia/src/components/tabs/Tabs.d.ts index 0bcbb44a8d7..2e5e5b1c741 100644 --- a/packages/dnb-eufemia/src/components/tabs/Tabs.d.ts +++ b/packages/dnb-eufemia/src/components/tabs/Tabs.d.ts @@ -20,7 +20,10 @@ export type TabsContent = | Record | React.ReactNode | ((...args: any[]) => any); -export type TabsTabElement = DynamicElement; +export type TabsTabElement = DynamicElement< + null, + ButtonProps | AnchorAllProps +>; export type TabsSelectedKey = string | number; export type TabsAlign = 'left' | 'center' | 'right'; export type TabsChildren = 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..530f379a1b6 100644 --- a/packages/dnb-eufemia/src/shared/types.tsx +++ b/packages/dnb-eufemia/src/shared/types.tsx @@ -25,8 +25,8 @@ export type DataAttributeTypes = { } export type DynamicElement< - P = React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - >, + E = HTMLElement, + P = React.DetailedHTMLProps, E>, > = keyof JSX.IntrinsicElements | React.FunctionComponent

+ +export type DynamicElementParams> = T