From cdd92a88115f9ba5b6a5265bf495640178f02b9c Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:39:54 +0200 Subject: [PATCH 01/46] feat(closing-button): align size and icon behavior with the design #43 (#481) * feat(closing-button): align size and icon behavior with the design #43 BREAKING CHANGE: size, iconSize changes, new default values * fix: migrate ClosingButton in other components due to changes #43 * fix(closing-button): align stories more like in figma #43 * fix(textfield): group casting iconSizes as const #43 --- .../components/header-modal/header-modal.tsx | 2 +- .../directions/direction-item.tsx | 1 + .../right-panel/right-panel.tsx | 2 +- .../components/map-components/sheet/sheet.tsx | 2 +- src/community/components/modal/modal.tsx | 2 +- .../closing-button/closing-button.module.scss | 12 +++--- .../closing-button/closing-button.spec.tsx | 42 +++++++++++-------- .../closing-button/closing-button.stories.tsx | 41 +++++++++++++++++- .../buttons/closing-button/closing-button.tsx | 21 +++++++--- .../form/file-dropzone/file-dropzone.tsx | 6 ++- .../form/file-upload/file-upload.tsx | 2 +- .../components/select-clear-indicator.tsx | 2 +- .../components/select-multi-value-remove.tsx | 1 + .../components/form/textfield/textfield.tsx | 7 +++- .../components/notifications/alert/alert.tsx | 2 +- .../overlays/popover/popover-content.tsx | 2 +- src/tedi/components/tags/tag/tag.tsx | 2 +- 17 files changed, 106 insertions(+), 43 deletions(-) diff --git a/src/community/components/layout/header/components/header-modal/header-modal.tsx b/src/community/components/layout/header/components/header-modal/header-modal.tsx index df3b57160..5794e99ae 100644 --- a/src/community/components/layout/header/components/header-modal/header-modal.tsx +++ b/src/community/components/layout/header/components/header-modal/header-modal.tsx @@ -55,7 +55,7 @@ export const HeaderModal = (props: HeaderModalProps) => { diff --git a/src/community/components/map-components/directions/direction-item.tsx b/src/community/components/map-components/directions/direction-item.tsx index 6ce1b16a3..18696e06e 100644 --- a/src/community/components/map-components/directions/direction-item.tsx +++ b/src/community/components/map-components/directions/direction-item.tsx @@ -61,6 +61,7 @@ export const DirectionItem = (props: DirectionItemProps): JSX.Element => { className={styles['tedi-directions__remove']} onClick={onDelete} aria-label={getLabel('remove')} + iconSize={18} /> )} diff --git a/src/community/components/map-components/right-panel/right-panel.tsx b/src/community/components/map-components/right-panel/right-panel.tsx index 65a2be2b2..d6b47b2c8 100644 --- a/src/community/components/map-components/right-panel/right-panel.tsx +++ b/src/community/components/map-components/right-panel/right-panel.tsx @@ -69,7 +69,7 @@ type DefaultCloseButtonProps = { id: string; isSingleItem: boolean; onClose: (id export const DefaultCloseButton = ({ id, isSingleItem, onClose }: DefaultCloseButtonProps): JSX.Element => ( onClose(id)} title="Sulge aken" /> diff --git a/src/community/components/map-components/sheet/sheet.tsx b/src/community/components/map-components/sheet/sheet.tsx index 4e6215294..cf64b65be 100644 --- a/src/community/components/map-components/sheet/sheet.tsx +++ b/src/community/components/map-components/sheet/sheet.tsx @@ -229,7 +229,7 @@ export const Sheet = (props: SheetProps): JSX.Element | null => {
{actions} diff --git a/src/community/components/modal/modal.tsx b/src/community/components/modal/modal.tsx index 2948f7dc1..122ac4b7d 100644 --- a/src/community/components/modal/modal.tsx +++ b/src/community/components/modal/modal.tsx @@ -109,7 +109,7 @@ export const Modal = (props: ModalProps): JSX.Element | null => { children: ( <> - + {child.props.children} diff --git a/src/tedi/components/buttons/closing-button/closing-button.module.scss b/src/tedi/components/buttons/closing-button/closing-button.module.scss index 16f010cfb..3004c36a7 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.module.scss +++ b/src/tedi/components/buttons/closing-button/closing-button.module.scss @@ -24,13 +24,13 @@ outline-offset: -2px; } - &--medium { - width: var(--button-xs-icon-size); - height: var(--button-xs-icon-size); - } - - &--large { + &--default { width: var(--button-sm-icon-size); height: var(--button-sm-icon-size); } + + &--small { + width: var(--button-xs-icon-size); + height: var(--button-xs-icon-size); + } } diff --git a/src/tedi/components/buttons/closing-button/closing-button.spec.tsx b/src/tedi/components/buttons/closing-button/closing-button.spec.tsx index 8bc45184d..ff51a20ef 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.spec.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.spec.tsx @@ -12,7 +12,6 @@ describe('ClosingButton component', () => { it('renders the ClosingButton with default props', () => { render( { console.log('Button pressed'); }} @@ -22,41 +21,48 @@ describe('ClosingButton component', () => { const button = screen.getByRole('button', { name: /close/i }); expect(button).toBeInTheDocument(); expect(button).toHaveClass('tedi-closing-button'); + expect(button).toHaveClass('tedi-closing-button--default'); const icon = button.querySelector('span[data-name="icon"]'); expect(icon).toBeInTheDocument(); - expect(icon).toHaveClass('tedi-icon--size-18'); + expect(icon).toHaveClass('tedi-icon--size-24'); }); - it('renders with the correct large size class and icon size', () => { - render( - { - console.log('Button pressed'); - }} - /> - ); + it('renders with small size when size="small"', () => { + render(); const button = screen.getByRole('button', { name: /close/i }); - expect(button).toBeInTheDocument(); - expect(button).toHaveClass('tedi-closing-button tedi-closing-button--large'); + expect(button).toHaveClass('tedi-closing-button--small'); const icon = button.querySelector('span[data-name="icon"]'); expect(icon).toBeInTheDocument(); + // Small button still uses default icon size expect(icon).toHaveClass('tedi-icon--size-24'); }); + it('forces small size when iconSize is 18', () => { + render(); + + const button = screen.getByRole('button', { name: /close/i }); + expect(button).toHaveClass('tedi-closing-button--small'); + + const icon = button.querySelector('span[data-name="icon"]'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('tedi-icon--size-18'); + }); + it('applies custom class names', () => { - render(); + render(); const button = screen.getByRole('button', { name: /Close/i }); - expect(button).toHaveClass('tedi-closing-button tedi-closing-button--large custom-class'); + expect(button).toHaveClass('tedi-closing-button'); + expect(button).toHaveClass('tedi-closing-button--default'); + expect(button).toHaveClass('custom-class'); }); it('triggers onClick handler when clicked', () => { const onClickMock = jest.fn(); - render(); + render(); const button = screen.getByRole('button', { name: /Close/i }); fireEvent.click(button); @@ -64,7 +70,7 @@ describe('ClosingButton component', () => { }); it('uses fallback label from label provider when title is not provided', () => { - render(); + render(); const button = screen.getByRole('button', { name: /Close/i }); expect(button).toHaveAttribute('title', 'Close'); @@ -72,7 +78,7 @@ describe('ClosingButton component', () => { }); it('uses custom title if provided', () => { - render(); + render(); const button = screen.getByRole('button', { name: /Custom Close/i }); expect(button).toHaveAttribute('title', 'Custom Close'); diff --git a/src/tedi/components/buttons/closing-button/closing-button.stories.tsx b/src/tedi/components/buttons/closing-button/closing-button.stories.tsx index 69528a74f..2055e2d08 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.stories.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.stories.tsx @@ -19,7 +19,8 @@ const meta: Meta = { }, }; -const sizeArray: ClosingButtonProps['size'][] = ['medium', 'large']; +const sizeArray: ClosingButtonProps['size'][] = ['default', 'small']; +const iconSizeArray: ClosingButtonProps['iconSize'][] = [18, 24]; export default meta; type Story = StoryObj; @@ -58,10 +59,34 @@ const StatesTemplate: StoryFn = () => { ); }; +const IconSizeTemplate: StoryFn = () => { + return ( +
+ {iconSizeArray.map((iconSize, key) => ( + + {`${iconSize}px`} + + alert(`${iconSize}px icon clicked`)} className="hover" /> + {iconSize === 24 && ( +
+ alert(`${iconSize}px icon clicked`)} + className="hover" + /> +
+ )} + +
+ ))} +
+ ); +}; + export const Default: Story = { args: { title: 'close', - size: 'medium', }, }; @@ -69,6 +94,18 @@ export const Size: Story = { render: SizeTemplate, }; +/** + * Hover state is shown on all buttons for size preview. + */ +export const IconSizes: Story = { + render: IconSizeTemplate, + parameters: { + pseudo: { + hover: '.hover', + }, + }, +}; + export const States: Story = { render: StatesTemplate, parameters: { diff --git a/src/tedi/components/buttons/closing-button/closing-button.tsx b/src/tedi/components/buttons/closing-button/closing-button.tsx index b718964f6..4e9f60d87 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.tsx @@ -6,7 +6,8 @@ import { useLabels } from '../../../providers/label-provider'; import { Icon } from '../../base/icon/icon'; import styles from './closing-button.module.scss'; -type ClosingButtonSize = 'medium' | 'large'; +export type ClosingButtonSize = 'default' | 'small'; +export type ClosingButtonIconSize = 18 | 24; export interface ClosingButtonProps extends ButtonHTMLAttributes { /** @@ -15,7 +16,7 @@ export interface ClosingButtonProps extends ButtonHTMLAttributes { const { getLabel } = useLabels(); - const { title = getLabel('close'), onClick, size = 'medium', className, ...rest } = props; + const { title = getLabel('close'), onClick, size = 'default', iconSize = 24, className, ...rest } = props; + + const resolvedSize: ClosingButtonSize = iconSize === 18 ? 'small' : size; const buttonClass = cn( styles['tedi-closing-button'], { - [styles[`tedi-closing-button--${size}`]]: size, + [styles[`tedi-closing-button--${resolvedSize}`]]: resolvedSize, }, className ); - const iconSize = size === 'large' ? 24 : 18; + const resolvedIconSize: ClosingButtonIconSize = iconSize ?? 24; return ( ); }; diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.tsx index 318c2cecd..bd49f42dc 100644 --- a/src/tedi/components/form/file-dropzone/file-dropzone.tsx +++ b/src/tedi/components/form/file-dropzone/file-dropzone.tsx @@ -106,7 +106,11 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => { {file.isValid === false && } - onFileRemove(file)} /> + onFileRemove(file)} + /> diff --git a/src/tedi/components/form/file-upload/file-upload.tsx b/src/tedi/components/form/file-upload/file-upload.tsx index b2f5790cb..2adb48c21 100644 --- a/src/tedi/components/form/file-upload/file-upload.tsx +++ b/src/tedi/components/form/file-upload/file-upload.tsx @@ -233,7 +233,7 @@ export const FileUpload = (props: FileUploadProps): JSX.Element => { {getLabel('clear')} ) : ( - + )} diff --git a/src/tedi/components/form/select/components/select-clear-indicator.tsx b/src/tedi/components/form/select/components/select-clear-indicator.tsx index 7f1d24767..368284589 100644 --- a/src/tedi/components/form/select/components/select-clear-indicator.tsx +++ b/src/tedi/components/form/select/components/select-clear-indicator.tsx @@ -17,7 +17,7 @@ export const SelectClearIndicator = ({ return isClearIndicatorVisible ? ( <> - + {getLabel('clear')} diff --git a/src/tedi/components/form/select/components/select-multi-value-remove.tsx b/src/tedi/components/form/select/components/select-multi-value-remove.tsx index 8ed9a4e08..828e3a88b 100644 --- a/src/tedi/components/form/select/components/select-multi-value-remove.tsx +++ b/src/tedi/components/form/select/components/select-multi-value-remove.tsx @@ -28,6 +28,7 @@ export const SelectMultiValueRemove = ({ innerProps, data }: MultiValueRemovePro onClick={handleClick} onKeyDown={handleKeyDown} className={styles['tedi-select__multi-value-clear']} + iconSize={18} title={`${getLabel('clear')} ${data.label}`} /> diff --git a/src/tedi/components/form/textfield/textfield.tsx b/src/tedi/components/form/textfield/textfield.tsx index f94f5acb3..ff1de6a5c 100644 --- a/src/tedi/components/form/textfield/textfield.tsx +++ b/src/tedi/components/form/textfield/textfield.tsx @@ -10,6 +10,11 @@ import { FeedbackText, FeedbackTextProps } from '../feedback-text/feedback-text' import FormLabel, { FormLabelProps } from '../form-label/form-label'; import styles from './textfield.module.scss'; +const iconSizes = { + large: 24, + default: 18, +} as const; + type TextFieldBreakpointProps = { /** * Input field size @@ -333,7 +338,7 @@ export const TextField = forwardRef((props, const renderClearButton = React.useMemo(() => { const clearButtonProps = { - size: (size === 'large' ? 'large' : 'medium') as 'medium' | 'large', + iconSize: iconSizes[size === 'large' ? 'large' : 'default'], onClick: disabled ? () => {} : clearInput, disabled, 'aria-disabled': disabled || undefined, diff --git a/src/tedi/components/notifications/alert/alert.tsx b/src/tedi/components/notifications/alert/alert.tsx index a926d0aa6..18e1a7295 100644 --- a/src/tedi/components/notifications/alert/alert.tsx +++ b/src/tedi/components/notifications/alert/alert.tsx @@ -149,7 +149,7 @@ export const Alert = (props: AlertProps): JSX.Element | null => { {onClose && ( - + )} diff --git a/src/tedi/components/overlays/popover/popover-content.tsx b/src/tedi/components/overlays/popover/popover-content.tsx index 1086962e4..0dedfac18 100644 --- a/src/tedi/components/overlays/popover/popover-content.tsx +++ b/src/tedi/components/overlays/popover/popover-content.tsx @@ -45,7 +45,7 @@ export const PopoverContent = (props: PopoverContentProps) => { title, titleProps = { element: 'h4' }, close, - closeProps = { size: 'large' }, + closeProps = { size: 'default' }, } = props; const { onOpenChange } = useContext(OverlayContext); const titleId = useId(); diff --git a/src/tedi/components/tags/tag/tag.tsx b/src/tedi/components/tags/tag/tag.tsx index 61405973e..ed1194a15 100644 --- a/src/tedi/components/tags/tag/tag.tsx +++ b/src/tedi/components/tags/tag/tag.tsx @@ -71,7 +71,7 @@ export const Tag = (props: TagProps): JSX.Element => {
)} - {!isLoading && onClose && } + {!isLoading && onClose && } ); }; From b2e9cf8fa0b04058d7da29a4daaad7183b045728 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 30 Jan 2026 06:43:03 +0000 Subject: [PATCH 02/46] chore(release): 16.0.0-rc.1 # [16.0.0-rc.1](https://github.com/TEDI-Design-System/react/compare/react-15.0.0...react-16.0.0-rc.1) (2026-01-30) ### Features * **closing-button:** align size and icon behavior with the design [#43](https://github.com/TEDI-Design-System/react/issues/43) ([#481](https://github.com/TEDI-Design-System/react/issues/481)) ([cdd92a8](https://github.com/TEDI-Design-System/react/commit/cdd92a88115f9ba5b6a5265bf495640178f02b9c)) ### BREAKING CHANGES * **closing-button:** size, iconSize changes, new default values --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a29a7671..37d8bc256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [16.0.0-rc.1](https://github.com/TEDI-Design-System/react/compare/react-15.0.0...react-16.0.0-rc.1) (2026-01-30) + + +### Features + +* **closing-button:** align size and icon behavior with the design [#43](https://github.com/TEDI-Design-System/react/issues/43) ([#481](https://github.com/TEDI-Design-System/react/issues/481)) ([cdd92a8](https://github.com/TEDI-Design-System/react/commit/cdd92a88115f9ba5b6a5265bf495640178f02b9c)) + + +### BREAKING CHANGES + +* **closing-button:** size, iconSize changes, new default values + # [15.0.0](https://github.com/TEDI-Design-System/react/compare/react-14.3.0...react-15.0.0) (2026-01-29) From 2c1152a549aef92cb3225f092b0519c13440b756 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:29:01 +0200 Subject: [PATCH 03/46] feat(form-label): update label prop type to ReactNode #486 (#490) --- .../form/form-label/form-label.spec.tsx | 17 +++++++++++++++++ .../components/form/form-label/form-label.tsx | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/tedi/components/form/form-label/form-label.spec.tsx b/src/tedi/components/form/form-label/form-label.spec.tsx index 9c470d5aa..f3fbf6fb5 100644 --- a/src/tedi/components/form/form-label/form-label.spec.tsx +++ b/src/tedi/components/form/form-label/form-label.spec.tsx @@ -61,4 +61,21 @@ describe('FormLabel component', () => { expect(getByText('Form Label Text')).toBeInTheDocument(); }); + + it('renders a ReactNode label correctly', () => { + const { getByTestId } = render( + + Custom Node + + } + /> + ); + + const customLabel = getByTestId('custom-label'); + expect(customLabel).toBeInTheDocument(); + expect(customLabel).toHaveTextContent('Custom Node'); + }); }); diff --git a/src/tedi/components/form/form-label/form-label.tsx b/src/tedi/components/form/form-label/form-label.tsx index 287c92d6b..593316eb8 100644 --- a/src/tedi/components/form/form-label/form-label.tsx +++ b/src/tedi/components/form/form-label/form-label.tsx @@ -13,7 +13,7 @@ export interface FormLabelProps { /** * The text content of the label that describes the input field. */ - label: string; + label: React.ReactNode; /** * Controls the visibility of the label. * Use `true` to hide the label visually while maintaining its space in the layout, From d5235e28571586250faa25cb78c90d34a6dcfdb7 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 2 Feb 2026 08:32:15 +0000 Subject: [PATCH 04/46] chore(release): 16.0.0-rc.2 # [16.0.0-rc.2](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.1...react-16.0.0-rc.2) (2026-02-02) ### Features * **form-label:** update label prop type to ReactNode [#486](https://github.com/TEDI-Design-System/react/issues/486) ([#490](https://github.com/TEDI-Design-System/react/issues/490)) ([2c1152a](https://github.com/TEDI-Design-System/react/commit/2c1152a549aef92cb3225f092b0519c13440b756)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37d8bc256..2168b95af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.2](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.1...react-16.0.0-rc.2) (2026-02-02) + + +### Features + +* **form-label:** update label prop type to ReactNode [#486](https://github.com/TEDI-Design-System/react/issues/486) ([#490](https://github.com/TEDI-Design-System/react/issues/490)) ([2c1152a](https://github.com/TEDI-Design-System/react/commit/2c1152a549aef92cb3225f092b0519c13440b756)) + # [16.0.0-rc.1](https://github.com/TEDI-Design-System/react/compare/react-15.0.0...react-16.0.0-rc.1) (2026-01-30) From a569636453c865b142ec53761949b97afd436546 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:08:54 +0200 Subject: [PATCH 05/46] fix(checkbox,radio): pass required prop to label #115 (#498) * feat(form-label): update label prop type to ReactNode #486 * fix(checkbox,radio): pass required prop to label #115 --- .../components/form/checkbox/checkbox.spec.tsx | 6 ++++++ .../form/checkbox/checkbox.stories.tsx | 8 ++++++++ src/tedi/components/form/checkbox/checkbox.tsx | 16 +++++++--------- src/tedi/components/form/choice-input.types.ts | 6 +++++- src/tedi/components/form/radio/radio.spec.tsx | 6 ++++++ src/tedi/components/form/radio/radio.stories.tsx | 8 ++++++++ src/tedi/components/form/radio/radio.tsx | 16 +++++++--------- 7 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/tedi/components/form/checkbox/checkbox.spec.tsx b/src/tedi/components/form/checkbox/checkbox.spec.tsx index de72e2c29..7b5e44a42 100644 --- a/src/tedi/components/form/checkbox/checkbox.spec.tsx +++ b/src/tedi/components/form/checkbox/checkbox.spec.tsx @@ -210,4 +210,10 @@ describe('Checkbox component', () => { expect(label.click).toHaveBeenCalled(); }); + + it('renders required indicator when required prop is true', () => { + render(); + + expect(screen.getByText('*')).toBeInTheDocument(); + }); }); diff --git a/src/tedi/components/form/checkbox/checkbox.stories.tsx b/src/tedi/components/form/checkbox/checkbox.stories.tsx index 63cf55f77..cfc172832 100644 --- a/src/tedi/components/form/checkbox/checkbox.stories.tsx +++ b/src/tedi/components/form/checkbox/checkbox.stories.tsx @@ -161,6 +161,14 @@ export const States = () => { /> + + + Required + + + + + diff --git a/src/tedi/components/form/checkbox/checkbox.tsx b/src/tedi/components/form/checkbox/checkbox.tsx index b91ccb453..8a0d87354 100644 --- a/src/tedi/components/form/checkbox/checkbox.tsx +++ b/src/tedi/components/form/checkbox/checkbox.tsx @@ -34,6 +34,7 @@ export const Checkbox = (props: CheckboxProps): JSX.Element => { tooltip, invalid, size = 'default', + required, ...rest } = props; const [innerChecked, setInnerChecked] = React.useState(defaultChecked || false); @@ -53,7 +54,7 @@ export const Checkbox = (props: CheckboxProps): JSX.Element => { const helperId = helper ? helper.id ?? `${id}-helper` : undefined; const tooltipId = tooltip ? `${id}-tooltip` : undefined; - const LabelBEM = cn(styles['tedi-checkbox'], { [styles['tedi-checkbox--disabled']]: disabled }); + const LabelBEM = cn(styles['tedi-checkbox__label'], { [styles['tedi-checkbox--disabled']]: disabled }); return (
@@ -100,8 +101,8 @@ export const Checkbox = (props: CheckboxProps): JSX.Element => {
- - {label && typeof label === 'string' ? ( + {label && ( + { hideLabel={hideLabel} label={label} tooltip={tooltip} + required={required} /> - ) : ( - - )} - + + )} {helper && ( diff --git a/src/tedi/components/form/choice-input.types.ts b/src/tedi/components/form/choice-input.types.ts index 74106bbaa..815b432c3 100644 --- a/src/tedi/components/form/choice-input.types.ts +++ b/src/tedi/components/form/choice-input.types.ts @@ -8,7 +8,7 @@ export interface ChoiceInputProps { /** * Label text */ - label: string | React.ReactNode; + label: React.ReactNode; /** * Additional classes. */ @@ -61,4 +61,8 @@ export interface ChoiceInputProps { * Whether the input is marked as invalid. */ invalid?: boolean; + /** + * Whether the input is marked as required. + */ + required?: boolean; } diff --git a/src/tedi/components/form/radio/radio.spec.tsx b/src/tedi/components/form/radio/radio.spec.tsx index 9021d7256..8581dbd9a 100644 --- a/src/tedi/components/form/radio/radio.spec.tsx +++ b/src/tedi/components/form/radio/radio.spec.tsx @@ -222,4 +222,10 @@ describe('Radio component', () => { expect(label.click).toHaveBeenCalled(); }); + + it('renders required indicator when required prop is true', () => { + render(); + + expect(screen.getByText('*')).toBeInTheDocument(); + }); }); diff --git a/src/tedi/components/form/radio/radio.stories.tsx b/src/tedi/components/form/radio/radio.stories.tsx index ec0eb4ed2..7a1772ef4 100644 --- a/src/tedi/components/form/radio/radio.stories.tsx +++ b/src/tedi/components/form/radio/radio.stories.tsx @@ -131,6 +131,14 @@ export const States = () => { /> + + + Required + + + + + diff --git a/src/tedi/components/form/radio/radio.tsx b/src/tedi/components/form/radio/radio.tsx index 639c54161..9590b8ff4 100644 --- a/src/tedi/components/form/radio/radio.tsx +++ b/src/tedi/components/form/radio/radio.tsx @@ -26,6 +26,7 @@ export const Radio = (props: RadioProps): JSX.Element => { tooltip, size = 'default', invalid, + required, ...rest } = props; @@ -46,7 +47,7 @@ export const Radio = (props: RadioProps): JSX.Element => { const helperId = helper ? helper.id ?? `${id}-helper` : undefined; const tooltipId = tooltip ? `${id}-tooltip` : undefined; - const LabelBEM = cn(styles['tedi-radio'], { [styles['tedi-radio--disabled']]: disabled }); + const LabelBEM = cn(styles['tedi-radio__label'], { [styles['tedi-radio--disabled']]: disabled }); return (
@@ -78,8 +79,8 @@ export const Radio = (props: RadioProps): JSX.Element => { />
- - {label && typeof label === 'string' ? ( + {label && ( + { hideLabel={hideLabel} label={label} tooltip={tooltip} + required={required} /> - ) : ( - - )} - + + )} {helper && ( From d8969e00ebb6fc774892c2bc34ac8631c3ae00af Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 2 Feb 2026 11:12:11 +0000 Subject: [PATCH 06/46] chore(release): 16.0.0-rc.3 # [16.0.0-rc.3](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.2...react-16.0.0-rc.3) (2026-02-02) ### Bug Fixes * **checkbox,radio:** pass required prop to label [#115](https://github.com/TEDI-Design-System/react/issues/115) ([#498](https://github.com/TEDI-Design-System/react/issues/498)) ([a569636](https://github.com/TEDI-Design-System/react/commit/a569636453c865b142ec53761949b97afd436546)), closes [#486](https://github.com/TEDI-Design-System/react/issues/486) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2168b95af..990b52c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.3](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.2...react-16.0.0-rc.3) (2026-02-02) + + +### Bug Fixes + +* **checkbox,radio:** pass required prop to label [#115](https://github.com/TEDI-Design-System/react/issues/115) ([#498](https://github.com/TEDI-Design-System/react/issues/498)) ([a569636](https://github.com/TEDI-Design-System/react/commit/a569636453c865b142ec53761949b97afd436546)), closes [#486](https://github.com/TEDI-Design-System/react/issues/486) + # [16.0.0-rc.2](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.1...react-16.0.0-rc.2) (2026-02-02) From 27799cc2c4db6381f911ada2697e1bd0b53d2a4d Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:54:25 +0200 Subject: [PATCH 07/46] fix(choice-group): prevent calling onchange twice on card variant #484 (#502) --- .../components/choice-group-item/choice-group-item.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx index 4f919120b..1beb291ec 100644 --- a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx +++ b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx @@ -124,7 +124,10 @@ export const ChoiceGroupItem = (props: ExtendedChoiceGroupItemProps): React.Reac disabled={disabled} checked={isChecked} defaultChecked={currentValue === undefined ? props.defaultChecked : undefined} - onChange={(e) => onChangeHandler(value, e.target.checked)} + onChange={(e) => { + e.stopPropagation(); + onChangeHandler(value, e.target.checked); + }} className={styles['tedi-choice-group-item__input']} role={type === 'radio' ? 'radio' : undefined} aria-checked={isChecked} From e70b5078731786a477b0eafecbb4326b435aca1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B5nis=20Tobre?= Date: Mon, 9 Feb 2026 15:44:16 +0200 Subject: [PATCH 08/46] Add MIT License to the project --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..f05362f4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TEDI Design System + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 7ea1fedf754e4828df7b508f8cd9ab4428ad3fe6 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:56:55 +0200 Subject: [PATCH 09/46] fix(choice-group): prevent double change events and unify card click behavior #504 (#505) * fix(choice-group): prevent double change events and unify card click behavior #504 * fix(choice-group): fix ChoiceGroupItem test #504 * fix(choice-group): add more test to cover the line coverage #504 --- .../choice-group-item.spec.tsx | 76 +++++++++++++------ .../choice-group-item/choice-group-item.tsx | 16 ++-- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.spec.tsx b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.spec.tsx index 22e97fab7..1e2d8c945 100644 --- a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.spec.tsx +++ b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.spec.tsx @@ -33,33 +33,27 @@ describe('ChoiceGroupItem', () => { it('renders the radio input correctly', () => { renderWithContext(); - const radioInput = screen.getByRole('radio', { name: 'Test Label' }); + const radioInput = screen.getByLabelText('Test Label'); expect(radioInput).toBeInTheDocument(); + expect(radioInput).toHaveAttribute('type', 'radio'); }); it('renders the checkbox input correctly when type is checkbox', () => { renderWithContext({ type: 'checkbox' }); - const checkboxInput = screen.getByRole('checkbox', { name: 'Test Label' }); + const checkboxInput = screen.getByLabelText('Test Label'); expect(checkboxInput).toBeInTheDocument(); + expect(checkboxInput).toHaveAttribute('type', 'checkbox'); }); it('renders the card variant correctly', () => { renderWithContext({ variant: 'card' }); - const cardInput = screen.getByRole('radio', { name: 'Test Label' }); - expect(cardInput).toBeInTheDocument(); - }); - - it('calls onChange handler when input is clicked', () => { - const onChange = jest.fn(); - renderWithContext({ onChange }, { currentValue: '', name: 'test-name', onChange, inputType: 'radio' }); - const radioInput = screen.getByRole('radio', { name: 'Test Label' }); - fireEvent.click(radioInput); - expect(onChange).toHaveBeenCalledWith('test-value', true); + const input = screen.getByLabelText('Test Label'); + expect(input).toBeInTheDocument(); }); it('renders with disabled state correctly', () => { renderWithContext({ disabled: true }); - const radioInput = screen.getByRole('radio', { name: 'Test Label' }); + const radioInput = screen.getByLabelText('Test Label'); expect(radioInput).toBeDisabled(); }); @@ -76,20 +70,9 @@ describe('ChoiceGroupItem', () => { expect(indicator).toBeInTheDocument(); }); - it('calls onChange handler when card input is clicked', () => { - const onChange = jest.fn(); - renderWithContext( - { variant: 'card', onChange }, - { currentValue: '', name: 'test-name', onChange, inputType: 'radio' } - ); - const cardInput = screen.getByRole('radio', { name: 'Test Label' }); - fireEvent.click(cardInput); - expect(onChange).toHaveBeenCalledWith('test-value', true); - }); - it('renders the card variant with disabled state correctly', () => { renderWithContext({ variant: 'card', disabled: true }); - const cardInput = screen.getByRole('radio', { name: 'Test Label' }); + const cardInput = screen.getByLabelText('Test Label'); expect(cardInput).toBeDisabled(); }); @@ -99,4 +82,47 @@ describe('ChoiceGroupItem', () => { const helperText = screen.getByText('Helper text'); expect(helperText).toBeInTheDocument(); }); + + it('calls onChange handler when label is clicked', () => { + const onChange = jest.fn(); + renderWithContext({ onChange }, { currentValue: '', name: 'test-name', onChange, inputType: 'radio' }); + const label = screen.getByText('Test Label'); + fireEvent.click(label); + expect(onChange).toHaveBeenCalled(); + }); + + it('calls onChange handler when card is clicked', () => { + const onChange = jest.fn(); + renderWithContext( + { variant: 'card', onChange }, + { currentValue: '', name: 'test-name', onChange, inputType: 'radio' } + ); + + const card = screen.getByText('Test Label'); + fireEvent.click(card); + expect(onChange).toHaveBeenCalled(); + }); + + it('programmatically clicks the input when card background is clicked', () => { + renderWithContext({ variant: 'card' }); + + const input = screen.getByLabelText('Test Label') as HTMLInputElement; + const clickSpy = jest.spyOn(input, 'click'); + const card = input.closest('.tedi-choice-group-item') as HTMLElement; + fireEvent.click(card); + + expect(clickSpy).toHaveBeenCalledTimes(1); + clickSpy.mockRestore(); + }); + + it('does NOT programmatically click input when clicking the native input directly', () => { + const mockInputClick = jest.fn(); + const mockInput = { click: mockInputClick } as unknown as HTMLInputElement; + + jest.spyOn(document, 'getElementById').mockReturnValue(mockInput); + renderWithContext({ variant: 'card' }); + const input = screen.getByLabelText('Test Label'); + fireEvent.click(input); + expect(mockInputClick).not.toHaveBeenCalled(); + }); }); diff --git a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx index 1beb291ec..2d23bb548 100644 --- a/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx +++ b/src/tedi/components/form/choice-group/components/choice-group-item/choice-group-item.tsx @@ -83,16 +83,17 @@ export const ChoiceGroupItem = (props: ExtendedChoiceGroupItemProps): React.Reac const InputComponent = type === 'radio' ? Radio : Checkbox; - const handleClick = (e: React.MouseEvent) => { - if ((e.target as HTMLElement).tagName === 'LABEL') return; - if (!disabled && variant === 'card') { - onChangeHandler(value, !isChecked); - } - }; + const handleClick = (e: React.MouseEvent) => { + if (disabled || variant !== 'card') return; + + const target = e.target as HTMLElement; + if (target.closest('input, label')) return; + document.getElementById(id)?.click(); + }; return ( -
+
{variant === 'default' || showIndicator ? ( { - e.stopPropagation(); onChangeHandler(value, e.target.checked); }} className={styles['tedi-choice-group-item__input']} From bb0f900267eee16c1e20cb9a2fd88c1cfe7859f8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 10 Feb 2026 08:59:28 +0000 Subject: [PATCH 10/46] chore(release): 16.0.0-rc.4 # [16.0.0-rc.4](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.3...react-16.0.0-rc.4) (2026-02-10) ### Bug Fixes * **choice-group:** prevent calling onchange twice on card variant [#484](https://github.com/TEDI-Design-System/react/issues/484) ([#502](https://github.com/TEDI-Design-System/react/issues/502)) ([27799cc](https://github.com/TEDI-Design-System/react/commit/27799cc2c4db6381f911ada2697e1bd0b53d2a4d)) * **choice-group:** prevent double change events and unify card click behavior [#504](https://github.com/TEDI-Design-System/react/issues/504) ([#505](https://github.com/TEDI-Design-System/react/issues/505)) ([7ea1fed](https://github.com/TEDI-Design-System/react/commit/7ea1fedf754e4828df7b508f8cd9ab4428ad3fe6)) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 990b52c77..a852f47b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [16.0.0-rc.4](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.3...react-16.0.0-rc.4) (2026-02-10) + + +### Bug Fixes + +* **choice-group:** prevent calling onchange twice on card variant [#484](https://github.com/TEDI-Design-System/react/issues/484) ([#502](https://github.com/TEDI-Design-System/react/issues/502)) ([27799cc](https://github.com/TEDI-Design-System/react/commit/27799cc2c4db6381f911ada2697e1bd0b53d2a4d)) +* **choice-group:** prevent double change events and unify card click behavior [#504](https://github.com/TEDI-Design-System/react/issues/504) ([#505](https://github.com/TEDI-Design-System/react/issues/505)) ([7ea1fed](https://github.com/TEDI-Design-System/react/commit/7ea1fedf754e4828df7b508f8cd9ab4428ad3fe6)) + # [16.0.0-rc.3](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.2...react-16.0.0-rc.3) (2026-02-02) From 97e4c5acfe7da7d5b5465b6f56dca29c0ecf035e Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:15:41 +0200 Subject: [PATCH 11/46] chore: add stories for ChoiceGroup with icons in labels example #119 (#503) --- .../choice-group/choice-group.stories.tsx | 129 ++++++++++++------ 1 file changed, 87 insertions(+), 42 deletions(-) diff --git a/src/tedi/components/form/choice-group/choice-group.stories.tsx b/src/tedi/components/form/choice-group/choice-group.stories.tsx index 1f61927d7..2b211d093 100644 --- a/src/tedi/components/form/choice-group/choice-group.stories.tsx +++ b/src/tedi/components/form/choice-group/choice-group.stories.tsx @@ -49,11 +49,11 @@ interface GenerateItemsArgs { variant?: 'primary' | 'secondary'; withHelper?: boolean; withIndicator?: boolean; - extraLongTitle?: boolean; tooltip?: boolean; colProps?: ColProps; layout?: 'separated' | 'segmented'; justifyContent?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'; + withIcons?: boolean; } const generateItems = ({ @@ -66,36 +66,44 @@ const generateItems = ({ colProps, layout, justifyContent, -}: GenerateItemsArgs): ExtendedChoiceGroupItemProps[] => [ - { - id: `${inputType}-${variant}-value-${index * 10 + 1}-${withHelper}-${withIndicator}-${layout}`, - label: 'Text', - value: `${inputType}-${variant}-value-${index * 10 + 1}-${withHelper}-${withIndicator}-${layout}`, - ...(withHelper && { helper: { text: 'Description' } }), - colProps, - tooltip: tooltip ? 'Tooltip' : undefined, - justifyContent, - }, - { - id: `${inputType}-${variant}-value-${index * 10 + 2}-${withHelper}-${withIndicator}-${layout}`, - label: 'Text', - value: `${inputType}-${variant}-value-${index * 10 + 2}-${withHelper}-${withIndicator}-${layout}`, - ...(withHelper && { helper: { text: 'Description' } }), - colProps, - tooltip: tooltip ? 'Tooltip' : undefined, - justifyContent, - }, - { - id: `${inputType}-${variant}-value-${index * 10 + 3}-${withHelper}-${withIndicator}-${layout}`, - label: 'Text', - value: `${inputType}-${variant}-value-${index * 10 + 3}-${withHelper}-${withIndicator}-${layout}`, - ...(withHelper && { helper: { text: 'Description', type: 'error' } }), - disabled: true, - colProps, - tooltip: tooltip ? 'Tooltip' : undefined, - justifyContent, - }, -]; + withIcons = false, +}: GenerateItemsArgs): ExtendedChoiceGroupItemProps[] => { + const baseId = withIcons ? `icon-${inputType}-${variant}` : `${inputType}-${variant}`; + const icons = withIcons ? ['train', 'directions_walk', 'directions_car'] : []; + + return [1, 2, 3].map((i) => { + const itemIndex = index * 10 + i; + const suffix = `${withHelper}-${withIndicator}-${layout ?? 'default'}`; + + let label: React.ReactNode = 'Text'; + + if (withIcons) { + label = ( + + + {' Text'} + + ); + } + + return { + id: `${baseId}-value-${itemIndex}-${suffix}`, + label, + value: `${baseId}-value-${itemIndex}`, + ...(withHelper && { + helper: { + text: 'Description', + ...(i === 3 && { type: 'error' }), + }, + }), + disabled: i === 3, + colProps, + tooltip: tooltip ? 'Tooltip' : undefined, + justifyContent, + ...(withIcons && i === 1 && { defaultChecked: true }), + }; + }); +}; const renderGroup = ( inputType: 'radio' | 'checkbox', @@ -104,13 +112,14 @@ const renderGroup = ( withIndicator: boolean, layout: 'segmented' | 'separated', index: number, - justifyContent: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' + justifyContent: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' = 'start', + withIcons = false ) => ( - + ); -const renderChoiceGroups = (inputType: 'radio' | 'checkbox', layout: 'segmented' | 'separated') => ( +const renderChoiceGroups = (inputType: 'radio' | 'checkbox', layout: 'segmented' | 'separated', withIcons = false) => ( @@ -164,12 +175,12 @@ const renderChoiceGroups = (inputType: 'radio' | 'checkbox', layout: 'segmented' Secondary - {renderGroup(inputType, 'primary', false, true, layout, 1, 'start')} - {renderGroup(inputType, 'primary', true, true, layout, 2, 'start')} + {renderGroup(inputType, 'primary', false, true, layout, 1, 'start', withIcons)} + {renderGroup(inputType, 'primary', true, true, layout, 2, 'start', withIcons)} {inputType !== 'radio' || - (layout !== 'separated' && renderGroup(inputType, 'primary', false, false, layout, 3, 'start'))} + (layout !== 'separated' && renderGroup(inputType, 'primary', false, false, layout, 3, 'start', withIcons))} {inputType !== 'radio' || - (layout !== 'separated' && renderGroup(inputType, 'primary', true, false, layout, 4, 'start'))} + (layout !== 'separated' && renderGroup(inputType, 'primary', true, false, layout, 4, 'start', withIcons))} ); @@ -223,6 +234,40 @@ export const RadioCardSegmented = () => {renderChoiceGroups('ra export const RadioCardSeparated = () => {renderChoiceGroups('radio', 'separated')}; export const CheckboxCard = () => {renderChoiceGroups('checkbox', 'separated')}; +export const RadioCardWithIcon: Story = { + render: () => ( + + + + Primary + + + Secondary + + + {renderGroup('radio', 'primary', false, true, 'segmented', 1, 'start', true)} + {renderGroup('radio', 'secondary', true, true, 'segmented', 2, 'start', true)} + + ), +}; + +export const CheckboxCardWithIcon: Story = { + render: () => ( + + + + Primary + + + Secondary + + + {renderGroup('checkbox', 'primary', false, true, 'separated', 5, 'start', true)} + {renderGroup('checkbox', 'secondary', true, true, 'separated', 6, 'start', true)} + + ), +}; + export const WithError: Story = { render: function Render(args) { const [selectedValues, setSelectedValues] = useState([]); From 6ca2d7fb3b5f609f3f51b884db074ee30fae457a Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:15:31 +0200 Subject: [PATCH 12/46] feat(info-button): added inverted variant and new color prop #420 (#508) * feat(info-button): added inverted variant and new color prop #420 * chore: trigger info button branch building #420 --- .../info-button/info-button.module.scss | 8 +++++ .../buttons/info-button/info-button.spec.tsx | 26 +++++++++++++++ .../info-button/info-button.stories.tsx | 33 ++++++++++++++++--- .../buttons/info-button/info-button.tsx | 16 ++++++--- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/tedi/components/buttons/info-button/info-button.module.scss b/src/tedi/components/buttons/info-button/info-button.module.scss index 6f2fc8dbf..8a7ac2a79 100644 --- a/src/tedi/components/buttons/info-button/info-button.module.scss +++ b/src/tedi/components/buttons/info-button/info-button.module.scss @@ -3,4 +3,12 @@ height: var(--button-xs-icon-size); min-height: var(--button-xs-icon-size); vertical-align: bottom; + + &[data-variant='inverted']:hover:not(:disabled) { + background-color: var(--button-main-neutral-inverted-icon-only-background-hover); + } + + &[data-variant='inverted']:active:not(:disabled) { + background-color: var(--button-main-neutral-inverted-icon-only-background-active); + } } diff --git a/src/tedi/components/buttons/info-button/info-button.spec.tsx b/src/tedi/components/buttons/info-button/info-button.spec.tsx index b450f9a3c..ad642916e 100644 --- a/src/tedi/components/buttons/info-button/info-button.spec.tsx +++ b/src/tedi/components/buttons/info-button/info-button.spec.tsx @@ -66,4 +66,30 @@ describe('InfoButton Component', () => { expect(button).toHaveAttribute('title', 'Info button'); expect(button).toHaveAttribute('id', 'info-button-id'); }); + + it('applies default color variant correctly', () => { + render(Info); + const button = screen.getByRole('button'); + expect(button).not.toHaveAttribute('data-variant'); + }); + + it('applies inverted color variant correctly', () => { + render(Info); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('data-variant', 'inverted'); + }); + + it('applies the inverted variant and still renders children', () => { + render( + + Child content + + ); + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('data-variant', 'inverted'); + + const child = screen.getByTestId('child'); + expect(child).toBeInTheDocument(); + expect(child).toHaveTextContent('Child content'); + }); }); diff --git a/src/tedi/components/buttons/info-button/info-button.stories.tsx b/src/tedi/components/buttons/info-button/info-button.stories.tsx index 6cf4a3cb0..2f5f1869e 100644 --- a/src/tedi/components/buttons/info-button/info-button.stories.tsx +++ b/src/tedi/components/buttons/info-button/info-button.stories.tsx @@ -28,20 +28,29 @@ const buttonStateArray = ['Default', 'Hover', 'Active', 'Focus']; type TemplateMultipleProps = { array: typeof buttonStateArray; + color?: 'default' | 'inverted'; }; const TemplateColumn: StoryFn = (args) => { - const { array, ...buttonProps } = args; + const { array, color = 'default', ...buttonProps } = args; return ( {array.map((state, index) => ( - {state} + + {state} + - + Info button @@ -57,7 +66,7 @@ export const Default: Story = { }, }; -export const InfoButtonStates: StoryObj = { +export const States: StoryObj = { render: TemplateColumn, args: { array: buttonStateArray, @@ -70,3 +79,19 @@ export const InfoButtonStates: StoryObj = { }, }, }; + +export const Inverted: StoryObj = { + render: TemplateColumn, + args: { + array: buttonStateArray, + color: 'inverted', + }, + parameters: { + pseudo: { + hover: '#Hover', + active: '#Active', + focusVisible: '#Focus', + }, + backgrounds: { default: 'brand' }, + }, +}; diff --git a/src/tedi/components/buttons/info-button/info-button.tsx b/src/tedi/components/buttons/info-button/info-button.tsx index 69d524cef..0669f1288 100644 --- a/src/tedi/components/buttons/info-button/info-button.tsx +++ b/src/tedi/components/buttons/info-button/info-button.tsx @@ -4,27 +4,33 @@ import React from 'react'; import { Button, ButtonProps } from '../button/button'; import styles from './info-button.module.scss'; -export interface InfoButtonProps extends Omit { +export interface InfoButtonProps extends Omit { /** - * If true, applies a small size to the InfoButton. + * If true, applies a small size to the InfoButton * @default false */ isSmall?: boolean; /** - * Children elements to be rendered inside the InfoButton. + * Children elements to be rendered inside the InfoButton */ children?: React.ReactNode; + /* + * Color variant of the InfoButton + * @default 'default' + */ + color?: 'default' | 'inverted'; } export const InfoButton = React.forwardRef( - ({ isSmall, children, ...props }, ref): JSX.Element => ( + ({ isSmall, children, color = 'default', ...props }, ref): JSX.Element => ( + + Content + + ); + + const btn = screen.getByTestId('btn-child'); + expect(btn).not.toHaveAttribute('aria-label', getLabel('tooltip.icon-trigger')); + }); + + it('adds tabindex=0 to non-Icon valid elements (cloneElement path)', () => { + render( + + + Hover me + + Content + + ); + + const span = screen.getByTestId('span-child'); + expect(span).toHaveAttribute('tabindex', '0'); + }); + + it('does NOT add aria-describedby when role is not "tooltip"', () => { + render( + + +
Trigger
+
+ Dialog content +
+ ); + + const trigger = screen.getByTestId('trigger-dialog'); + expect(trigger).not.toHaveAttribute('aria-describedby'); + }); + + it.each([ + { role: 'tooltip' as const, open: true, expectDescribedBy: true }, + { role: 'tooltip' as const, open: false, expectDescribedBy: false }, + { role: 'dialog' as const, open: true, expectDescribedBy: false }, + ])('aria-describedby behavior - role=$role open=$open → $expectDescribedBy', ({ role, open, expectDescribedBy }) => { + const onToggle = jest.fn(); + + render( + + +
Trigger
+
+ Content +
+ ); + + const trigger = screen.getByTestId('trigger'); + + if (expectDescribedBy) { + const content = screen.getByTestId('overlay-content'); + expect(trigger).toHaveAttribute('aria-describedby', content.id); + } else { + expect(trigger).not.toHaveAttribute('aria-describedby'); + } + }); +}); diff --git a/src/tedi/components/overlays/overlay/overlay-trigger.tsx b/src/tedi/components/overlays/overlay/overlay-trigger.tsx index 9fb4dc97e..936305cba 100644 --- a/src/tedi/components/overlays/overlay/overlay-trigger.tsx +++ b/src/tedi/components/overlays/overlay/overlay-trigger.tsx @@ -21,9 +21,15 @@ export interface OverlayTriggerProps { export const OverlayTrigger = (props: OverlayTriggerProps) => { const { children, className } = props; const { getLabel } = useLabels(); - const { getReferenceProps, reference, openWith } = useContext(OverlayContext); + const { getReferenceProps, reference, openWith, open, role, contentId } = useContext(OverlayContext); // eslint-disable-next-line @typescript-eslint/no-explicit-any const refs = useMergeRefs([reference, (children as React.ComponentPropsWithRef).ref]); + const extraProps = + role === 'tooltip' + ? { + 'aria-describedby': open ? contentId : undefined, + } + : {}; if (isValidElement(children)) { return cloneElement( @@ -31,6 +37,7 @@ export const OverlayTrigger = (props: OverlayTriggerProps) => { getReferenceProps({ ref: refs, tabIndex: 0, + ...extraProps, label: children.type === Icon ? getLabel('tooltip.icon-trigger') : undefined, ...children.props, }) @@ -50,6 +57,7 @@ export const OverlayTrigger = (props: OverlayTriggerProps) => { }, className ), + ...extraProps, })} > {children} diff --git a/src/tedi/components/overlays/overlay/overlay.spec.tsx b/src/tedi/components/overlays/overlay/overlay.spec.tsx index c9c38bf48..34ef8eef7 100644 --- a/src/tedi/components/overlays/overlay/overlay.spec.tsx +++ b/src/tedi/components/overlays/overlay/overlay.spec.tsx @@ -207,4 +207,49 @@ describe('Overlay component', () => { expect(document.body.style.overflow).toBe(''); }); }); + + it('does NOT add aria-label when child is valid element but NOT an Icon (cloneElement path)', () => { + const { getLabel } = useLabels(); + + render( + + + Hover me + + Content + + ); + + const span = screen.getByTestId('span-child'); + + expect(span).toHaveAttribute('tabindex', '0'); + expect(span).not.toHaveAttribute('aria-label'); + expect(span).not.toHaveAttribute('aria-label', getLabel('tooltip.icon-trigger')); + }); + + it('removes aria-describedby when tooltip is closed', async () => { + render( + + +
Trigger div
+
+ Hidden tooltip +
+ ); + + const trigger = screen.getByTestId('div-trigger'); + expect(trigger).not.toHaveAttribute('aria-describedby'); + + await act(async () => { + fireEvent.mouseEnter(trigger); + }); + + expect(trigger).toHaveAttribute('aria-describedby'); + + await act(async () => { + fireEvent.mouseLeave(trigger); + }); + + expect(trigger).not.toHaveAttribute('aria-describedby'); + }); }); diff --git a/src/tedi/components/overlays/overlay/overlay.tsx b/src/tedi/components/overlays/overlay/overlay.tsx index 279258685..338062c87 100644 --- a/src/tedi/components/overlays/overlay/overlay.tsx +++ b/src/tedi/components/overlays/overlay/overlay.tsx @@ -20,7 +20,7 @@ import { useRole, UseRoleProps, } from '@floating-ui/react'; -import { ComponentProps, createContext, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; +import { ComponentProps, createContext, ReactNode, useCallback, useId, useMemo, useRef, useState } from 'react'; import { useIsMounted, useIsTouchDevice } from '../../../helpers'; import { OverlayContent } from './overlay-content'; @@ -117,6 +117,8 @@ export interface OverlayContextType { placement: Placement; context: FloatingContext; scrollLock?: boolean; + role?: UseRoleProps['role']; + contentId: string; } export const OverlayContext = createContext({ @@ -143,6 +145,7 @@ export const OverlayContext = createContext({ placement: 'top', context: {} as FloatingContext, scrollLock: undefined, + contentId: '', }); export const Overlay = (props: OverlayProps) => { @@ -220,42 +223,70 @@ export const Overlay = (props: OverlayProps) => { }), ]); - return ( - - {children} - + const contentId = useId(); + const contextValue = useMemo( + () => ({ + open: isOpen, + onOpenChange, + isMounted, + openWith, + focusManager: focusManager + ? { + order, + modal, + initialFocus: resolvedInitialFocus, + ...restFocusManager, + } + : undefined, + reference: refs.setReference, + floating: refs.setFloating, + arrowRef, + x, + y, + strategy, + getReferenceProps, + getFloatingProps, + arrow: { + width: arrowDimensions?.width, + height: arrowDimensions?.height, + ...middlewareData.arrow, + }, + context, + placement, + scrollLock, + role, + contentId, + }), + [ + isOpen, + onOpenChange, + isMounted, + openWith, + focusManager, + refs.setReference, + refs.setFloating, + arrowRef, + x, + y, + strategy, + getReferenceProps, + getFloatingProps, + arrowDimensions?.width, + arrowDimensions?.height, + middlewareData.arrow, + context, + placement, + scrollLock, + role, + contentId, + modal, + order, + resolvedInitialFocus, + restFocusManager, + ] ); + + return {children}; }; Overlay.Trigger = OverlayTrigger; diff --git a/src/tedi/components/overlays/tooltip/tooltip.tsx b/src/tedi/components/overlays/tooltip/tooltip.tsx index 916df9ef9..554435b69 100644 --- a/src/tedi/components/overlays/tooltip/tooltip.tsx +++ b/src/tedi/components/overlays/tooltip/tooltip.tsx @@ -22,7 +22,7 @@ export interface TooltipProps } export const Tooltip = (props: TooltipProps) => { - const { openWith = 'hover', ...rest } = props; + const { openWith = 'hover', focusManager, ...rest } = props; return ( { height: ARROW_HEIGHT, }} openWith={openWith} + role="tooltip" + focusManager={{ + modal: false, + order: ['reference', 'content'], + returnFocus: true, + ...focusManager, + }} {...rest} /> ); From e518a826088d9af65c750931fce8bb11ca0ed794 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 16 Feb 2026 06:25:41 +0000 Subject: [PATCH 19/46] chore(release): 16.0.0-rc.8 # [16.0.0-rc.8](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.7...react-16.0.0-rc.8) (2026-02-16) ### Bug Fixes * **tooltip,overlay:** wcag improvements [#468](https://github.com/TEDI-Design-System/react/issues/468) ([#491](https://github.com/TEDI-Design-System/react/issues/491)) ([fa60e46](https://github.com/TEDI-Design-System/react/commit/fa60e46c2492734c29d3af2ac3d7cb4df9779c69)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b745dec..fb6f5f412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.8](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.7...react-16.0.0-rc.8) (2026-02-16) + + +### Bug Fixes + +* **tooltip,overlay:** wcag improvements [#468](https://github.com/TEDI-Design-System/react/issues/468) ([#491](https://github.com/TEDI-Design-System/react/issues/491)) ([fa60e46](https://github.com/TEDI-Design-System/react/commit/fa60e46c2492734c29d3af2ac3d7cb4df9779c69)) + # [16.0.0-rc.7](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.6...react-16.0.0-rc.7) (2026-02-13) From 7fa9356332e6fa49ed21aef5c328fb50b555b9bb Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:29:22 +0200 Subject: [PATCH 20/46] chore: increase branch retention to 30 days #514 (#515) --- .github/workflows/list-storybook-branches.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/list-storybook-branches.sh b/.github/workflows/list-storybook-branches.sh index d2d5c9eed..a28b2a405 100755 --- a/.github/workflows/list-storybook-branches.sh +++ b/.github/workflows/list-storybook-branches.sh @@ -1,7 +1,7 @@ #!/bin/bash # Number of days to check -DAYS=10 +DAYS=30 # Get the current date in seconds since epoch CURRENT_DATE=$(date +%s) From 35c4962ec56612c5e5b727a27f813eb96fd7fb1e Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:39:45 +0200 Subject: [PATCH 21/46] feat(alert): new size prop with default and small values #11 (#509) * feat(alert): new size prop with default and small values #11 * feat(alert): fix small font size, align content vertically center #11 * fix(alert): remove console log, clean up prop descriptions #11 --- .../notifications/alert/alert.module.scss | 24 ++++++++++-- .../notifications/alert/alert.spec.tsx | 19 ++++++++++ .../notifications/alert/alert.stories.tsx | 38 +++++++++++++++++++ .../components/notifications/alert/alert.tsx | 11 +++++- 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/tedi/components/notifications/alert/alert.module.scss b/src/tedi/components/notifications/alert/alert.module.scss index e84a27354..ba7710e83 100644 --- a/src/tedi/components/notifications/alert/alert.module.scss +++ b/src/tedi/components/notifications/alert/alert.module.scss @@ -5,8 +5,6 @@ } .tedi-alert { - padding: var(--alert-default-padding-y) var(--alert-default-padding-x); - font-size: var(--body-regular-size); border: 1px solid; border-radius: var(--alert-radius); @@ -14,14 +12,32 @@ &__content { display: flex; - gap: var(--layout-grid-gutters-08); - align-items: flex-start; + align-self: center; .tedi-alert__icon { line-height: var(--body-regular-line-height); } } + &--size-default { + padding: var(--alert-default-padding-y) var(--alert-default-padding-x); + font-size: var(--body-regular-size); + + .tedi-alert__content { + gap: var(--layout-grid-gutters-08); + } + } + + &--size-small { + padding: var(--alert-default-padding-y-sm) var(--alert-default-padding-x-sm); + font-size: var(--body-small-regular-size); + line-height: var(--body-small-regular-line-height); + + .tedi-alert__content { + gap: var(--layout-grid-gutters-04); + } + } + &--info { @include alert-variant(var(--alert-default-background-info), var(--alert-default-border-info)); } diff --git a/src/tedi/components/notifications/alert/alert.spec.tsx b/src/tedi/components/notifications/alert/alert.spec.tsx index 336858a85..ae944d37d 100644 --- a/src/tedi/components/notifications/alert/alert.spec.tsx +++ b/src/tedi/components/notifications/alert/alert.spec.tsx @@ -118,4 +118,23 @@ describe('Alert component', () => { const title = screen.getByText(text); expect(title.tagName).toBe(level.toUpperCase()); }); + + it('renders with small size and applies correct size class', () => { + render(Small Alert); + + const alert = screen.getByRole('alert'); + expect(alert).toHaveClass('tedi-alert--size-small'); + }); + + it('renders danger alert with small size', () => { + render( + + Danger Small Alert + + ); + + const alert = screen.getByRole('alert'); + expect(alert).toHaveClass('tedi-alert--danger'); + expect(alert).toHaveClass('tedi-alert--size-small'); + }); }); diff --git a/src/tedi/components/notifications/alert/alert.stories.tsx b/src/tedi/components/notifications/alert/alert.stories.tsx index ca3d894ff..51d7a59f4 100644 --- a/src/tedi/components/notifications/alert/alert.stories.tsx +++ b/src/tedi/components/notifications/alert/alert.stories.tsx @@ -1,5 +1,7 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { Text } from '../../base/typography/text/text'; +import { Col, Row } from '../../layout/grid'; import { VerticalSpacing } from '../../layout/vertical-spacing'; import Link from '../../navigation/link/link'; import Alert, { AlertProps } from '../alert/alert'; @@ -36,6 +38,8 @@ const alertTypes: { type: AlertProps['type']; icon: string }[] = [ { type: 'danger', icon: 'error' }, ]; +const sizeArray: AlertProps['size'][] = ['default', 'small']; + const TypesTemplate: StoryFn = (args) => ( {alertTypes.map(({ type, icon }) => ( @@ -46,6 +50,31 @@ const TypesTemplate: StoryFn = (args) => ( ); +interface TemplateMultipleProps extends AlertProps { + array: Type[]; +} + +const TemplateColumn: StoryFn = (args) => { + const { array, ...alertProps } = args; + + return ( +
+ {array.map((value, key) => ( + + + {value ? value.charAt(0).toUpperCase() + value.slice(1) : ''} + + + + Content description + + + + ))} +
+ ); +}; + const Template: StoryFn = (args) => ; export const Default: Story = { args: { @@ -69,6 +98,15 @@ const WithAndWithoutHeading: StoryFn = (args) => { ); }; +export const Sizes: StoryObj = { + render: TemplateColumn, + args: { + array: sizeArray, + type: 'info', + children: 'Content description', + onClose: () => null, + }, +}; export const Headless: Story = { render: Template, args: { diff --git a/src/tedi/components/notifications/alert/alert.tsx b/src/tedi/components/notifications/alert/alert.tsx index 18e1a7295..911e5826c 100644 --- a/src/tedi/components/notifications/alert/alert.tsx +++ b/src/tedi/components/notifications/alert/alert.tsx @@ -29,6 +29,13 @@ type AlertBreakpointProps = { * @default false */ noSideBorders?: boolean; + /** + * Alert size variant. + * - 'default': Standard alert size with padding and border radius. + * - 'small': More compact alert size with reduced padding. + * @default default + */ + size?: 'default' | 'small'; }; export interface AlertProps extends BreakpointSupport { @@ -79,7 +86,7 @@ export interface AlertProps extends BreakpointSupport { * // For secondary notifications * titleElement="h4" * - * @default 'h3' + * @default h3 */ titleElement?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; } @@ -97,12 +104,14 @@ export const Alert = (props: AlertProps): JSX.Element | null => { isGlobal = false, noSideBorders = false, titleElement = 'h3', + size = 'default', ...rest } = getCurrentBreakpointProps(props); const alertBEM = cn( styles['tedi-alert'], styles[`tedi-alert--${type}`], + styles[`tedi-alert--size-${size}`], { [styles['tedi-alert--global']]: isGlobal, [styles['tedi-alert--no-side-borders']]: noSideBorders, From d8a29276fad05101601d87abdc081bf1dadc5e24 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 08:43:33 +0000 Subject: [PATCH 22/46] chore(release): 16.0.0-rc.9 # [16.0.0-rc.9](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.8...react-16.0.0-rc.9) (2026-02-19) ### Features * **alert:** new size prop with default and small values [#11](https://github.com/TEDI-Design-System/react/issues/11) ([#509](https://github.com/TEDI-Design-System/react/issues/509)) ([35c4962](https://github.com/TEDI-Design-System/react/commit/35c4962ec56612c5e5b727a27f813eb96fd7fb1e)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb6f5f412..57a1d442b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.9](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.8...react-16.0.0-rc.9) (2026-02-19) + + +### Features + +* **alert:** new size prop with default and small values [#11](https://github.com/TEDI-Design-System/react/issues/11) ([#509](https://github.com/TEDI-Design-System/react/issues/509)) ([35c4962](https://github.com/TEDI-Design-System/react/commit/35c4962ec56612c5e5b727a27f813eb96fd7fb1e)) + # [16.0.0-rc.8](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.7...react-16.0.0-rc.8) (2026-02-16) From 73cd4f11348e4cccc9dfbfd84b7a333ce59f2023 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:58:24 +0200 Subject: [PATCH 23/46] feat(button-group): add size prop, add small example, fix examples on storybook #14 (#511) * feat(button-group): add size prop, add small example, fix examples on storybook #14 * fix(button-group): improve css readablity #14 --- .../button-group/button-group.module.scss | 25 +++++++++- .../button-group/button-group.stories.tsx | 50 +++++++++++++++++-- .../buttons/button-group/button-group.tsx | 17 ++++++- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/tedi/components/buttons/button-group/button-group.module.scss b/src/tedi/components/buttons/button-group/button-group.module.scss index fe4ab8aee..61910972d 100644 --- a/src/tedi/components/buttons/button-group/button-group.module.scss +++ b/src/tedi/components/buttons/button-group/button-group.module.scss @@ -11,8 +11,6 @@ align-items: center; justify-content: center; height: auto; - min-height: 2.5rem; - padding: calc(var(--button-md-padding-y) - 1px) var(--button-md-padding-x); text-align: center; cursor: pointer; border-radius: 0; @@ -38,6 +36,17 @@ @include breakpoints.media-breakpoint-down(md) { flex: 1; } + + &--size-default { + min-height: var(--button-md-height); + padding: calc(var(--button-md-padding-y) - 1px) var(--button-md-padding-x); + } + + &--size-small { + min-height: var(--button-sm-height); + padding: calc(var(--button-sm-padding-y) - 1px) var(--button-sm-padding-x); + font-size: var(--button-text-size-sm); + } } } @@ -65,6 +74,12 @@ background-color: var(--button-group-primary-selected-background); border-color: var(--button-group-primary-selected-border); } + + &:focus-visible:not(:disabled, .tedi-button-group__item--active) { + color: var(--button-group-primary-inactive-text); + background-color: var(--button-group-primary-inactive-background); + border: 1px solid var(--button-group-primary-inactive-border); + } } } @@ -93,6 +108,12 @@ outline: 2px solid var(--button-group-secondary-selected-border); outline-offset: -2px; } + + &:focus-visible:not(:disabled, .tedi-button-group__item--active) { + color: var(--button-group-secondary-inactive-text); + background-color: var(--button-group-secondary-inactive-background); + border: 1px solid var(--button-group-secondary-inactive-border); + } } } diff --git a/src/tedi/components/buttons/button-group/button-group.stories.tsx b/src/tedi/components/buttons/button-group/button-group.stories.tsx index d3b692e0d..14adb9282 100644 --- a/src/tedi/components/buttons/button-group/button-group.stories.tsx +++ b/src/tedi/components/buttons/button-group/button-group.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; import { useState } from 'react'; +import { Text } from '../../base/typography/text/text'; import { Col, Row } from '../../layout/grid'; import { VerticalSpacing } from '../../layout/vertical-spacing'; import { Button } from '../button/button'; @@ -24,7 +25,8 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const buttonStates = ['Default', 'Hover', 'Active', 'Disabled']; +const buttonStates = ['Default', 'Hover', 'Active', 'Focus', 'Disabled']; +const sizeArray: ButtonGroupProps['size'][] = ['default', 'small']; const Template: StoryFn = (args) => ( @@ -36,6 +38,40 @@ const Template: StoryFn = (args) => ( ); +interface TemplateMultipleProps extends ButtonGroupProps { + array: Type[]; + property: keyof ButtonGroupProps; +} + +const TemplateSizes: StoryFn = (args) => { + const { array, property, ...buttonGroupProps } = args; + + return ( +
+ {array.map((value, key) => { + return ( + + + {value ? value.charAt(0).toUpperCase() + value.slice(1) : ''} + + + + + + + + + + + + ); + })} +
+ ); +}; + const TemplateTypes: StoryFn = (args) => { return ( @@ -73,6 +109,14 @@ export const Default: Story = { }, }; +export const Sizes: StoryObj = { + render: TemplateSizes, + args: { + property: 'size', + array: sizeArray, + }, +}; + export const Types: StoryObj = { render: TemplateTypes, }; @@ -130,7 +174,7 @@ export const Primary: StoryObj<{ states: string[] }> = { pseudo: { hover: ['#Hover-primary'], active: ['#Active-primary'], - focus: ['#Focus-primary'], + focusVisible: ['#Focus-primary'], }, }, }; @@ -144,7 +188,7 @@ export const Secondary: StoryObj<{ states: string[] }> = { pseudo: { hover: ['#Hover-secondary'], active: ['#Active-secondary'], - focus: ['#Focus-secondary'], + focusVisible: ['#Focus-secondary'], }, }, }; diff --git a/src/tedi/components/buttons/button-group/button-group.tsx b/src/tedi/components/buttons/button-group/button-group.tsx index b2e0ecbaa..f830c304c 100644 --- a/src/tedi/components/buttons/button-group/button-group.tsx +++ b/src/tedi/components/buttons/button-group/button-group.tsx @@ -36,10 +36,22 @@ export type ButtonGroupProps = { * Additional custom CSS classes to apply to the ButtonGroup container */ className?: string; + /** + * Size of the buttons in ButtonGroup + */ + size?: 'default' | 'small'; }; export const ButtonGroup = (props: ButtonGroupProps): JSX.Element => { - const { children, className, type = 'primary', onSelectionChange, stretch = false, ariaLabel } = props; + const { + children, + className, + type = 'primary', + onSelectionChange, + stretch = false, + ariaLabel, + size = 'default', + } = props; return (
{ className: cn(styles['tedi-button-group__item'], { [styles['tedi-button-group__item--active']]: child.props.isActive, [styles['tedi-button-group__item--disabled']]: child.props.disabled, + [styles[`tedi-button-group__item--size-${size}`]]: size, }), - noStyle: true, + size: size, onClick: () => { if (!child.props.disabled) { child.props.onClick?.(); From afbd222c327567551e6388d3ea6f4cf2772d9424 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 09:02:05 +0000 Subject: [PATCH 24/46] chore(release): 16.0.0-rc.10 # [16.0.0-rc.10](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.9...react-16.0.0-rc.10) (2026-02-19) ### Features * **button-group:** add size prop, add small example, fix examples on storybook [#14](https://github.com/TEDI-Design-System/react/issues/14) ([#511](https://github.com/TEDI-Design-System/react/issues/511)) ([73cd4f1](https://github.com/TEDI-Design-System/react/commit/73cd4f11348e4cccc9dfbfd84b7a333ce59f2023)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a1d442b..c6a49b487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.10](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.9...react-16.0.0-rc.10) (2026-02-19) + + +### Features + +* **button-group:** add size prop, add small example, fix examples on storybook [#14](https://github.com/TEDI-Design-System/react/issues/14) ([#511](https://github.com/TEDI-Design-System/react/issues/511)) ([73cd4f1](https://github.com/TEDI-Design-System/react/commit/73cd4f11348e4cccc9dfbfd84b7a333ce59f2023)) + # [16.0.0-rc.9](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.8...react-16.0.0-rc.9) (2026-02-19) From f32bd83b549b195ecb70b4f9191dd8ef9f6b0a42 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:04:20 +0200 Subject: [PATCH 25/46] fix(link): fix link icon sizes in relation to figma design #46 (#520) * fix(link): fix link icon sizes in relation to figma design #46 * fix(link): small icon size and responsive #46 --- package-lock.json | 8 +++---- package.json | 2 +- .../button-content/button-content.module.scss | 23 +++++++++++++++---- .../buttons/button-content/button-content.tsx | 4 +--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1604a4dfe..2a4e27d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@mui/material": "^5.15.13", "@mui/x-date-pickers": "^5.0.20", "@tanstack/react-table": "^8.13.2", - "@tedi-design-system/core": "3.0.1", + "@tedi-design-system/core": "3.2.0", "classnames": "^2.5.1", "draft-js": "^0.11.7", "draftjs-md-converter": "^1.5.2", @@ -7979,9 +7979,9 @@ } }, "node_modules/@tedi-design-system/core": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.0.1.tgz", - "integrity": "sha512-ioet8RlFmWjg8fic4WUuYeavLiqUsKx3vFGZzzXkL91xNNjHexNVKhhtMLLkpCywzOc2tKXMx3AYdDhu2dsbwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tedi-design-system/core/-/core-3.2.0.tgz", + "integrity": "sha512-R9gpmprRT8qCGeJ8Frhz2yiJQ9bJM1Yp3mvWHeX2a6600hWh3mKXvhARSLFAF6im83FEFmOoKwdIOr4nfy8Qbg==", "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" diff --git a/package.json b/package.json index 6fcde260d..3fba459cb 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@mui/material": "^5.15.13", "@mui/x-date-pickers": "^5.0.20", "@tanstack/react-table": "^8.13.2", - "@tedi-design-system/core": "3.0.1", + "@tedi-design-system/core": "3.2.0", "classnames": "^2.5.1", "draft-js": "^0.11.7", "draftjs-md-converter": "^1.5.2", diff --git a/src/tedi/components/buttons/button-content/button-content.module.scss b/src/tedi/components/buttons/button-content/button-content.module.scss index 27c5ab3c1..15a153cd0 100644 --- a/src/tedi/components/buttons/button-content/button-content.module.scss +++ b/src/tedi/components/buttons/button-content/button-content.module.scss @@ -399,11 +399,16 @@ $btn-width-large: 3.72rem; height: auto; min-height: auto; padding: 0; + font-size: var(--body-link-size-regular); text-align: left; text-decoration: none; background: none; border: none; + &.tedi-btn--small { + font-size: var(--body-link-size-responsive); + } + @include link-variant( var(--link-primary-default), var(--link-primary-hover), @@ -411,12 +416,22 @@ $btn-width-large: 3.72rem; var(--link-primary-focus) ); - .tedi-btn__icon--left { - margin-right: var(--button-sm-inner-spacing); + .tedi-btn__inner { + .tedi-btn__icon--left { + display: inline; + margin-right: var(--button-sm-inner-spacing); + vertical-align: middle; + } + + .tedi-btn__icon--right { + display: inline; + margin-left: var(--button-sm-inner-spacing); + vertical-align: middle; + } } - .tedi-btn__icon--right { - margin-left: var(--button-sm-inner-spacing); + .tedi-btn__icon-responsive { + font-size: var(--link-icon-responsive); } } diff --git a/src/tedi/components/buttons/button-content/button-content.tsx b/src/tedi/components/buttons/button-content/button-content.tsx index dff0edabc..604d47c7a 100644 --- a/src/tedi/components/buttons/button-content/button-content.tsx +++ b/src/tedi/components/buttons/button-content/button-content.tsx @@ -144,14 +144,12 @@ const InternalButtonContent = forwardRef( const getIcon = (location: string, icon: string | IconWithoutBackgroundProps): JSX.Element => { const iconBEM = cn(styles['tedi-btn__icon'], styles[`tedi-btn__icon--${location}`], { [styles['tedi-btn__spinner']]: isLoading, + [styles['tedi-btn__icon-responsive']]: Component === 'a' && size === 'small', }); - const isLink = visualType === 'link'; - const defaultIconProps: Partial = { size: size === 'large' ? 24 : 18, className: iconBEM, - ...(isLink ? { display: 'inline' } : {}), }; const iconProps: IconWithoutBackgroundProps = From 7ac9916fdb750e1350785502b06c5cc02d608f53 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 09:09:21 +0000 Subject: [PATCH 26/46] chore(release): 16.0.0-rc.11 # [16.0.0-rc.11](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.10...react-16.0.0-rc.11) (2026-02-19) ### Bug Fixes * **link:** fix link icon sizes in relation to figma design [#46](https://github.com/TEDI-Design-System/react/issues/46) ([#520](https://github.com/TEDI-Design-System/react/issues/520)) ([f32bd83](https://github.com/TEDI-Design-System/react/commit/f32bd83b549b195ecb70b4f9191dd8ef9f6b0a42)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a49b487..6ee181fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.11](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.10...react-16.0.0-rc.11) (2026-02-19) + + +### Bug Fixes + +* **link:** fix link icon sizes in relation to figma design [#46](https://github.com/TEDI-Design-System/react/issues/46) ([#520](https://github.com/TEDI-Design-System/react/issues/520)) ([f32bd83](https://github.com/TEDI-Design-System/react/commit/f32bd83b549b195ecb70b4f9191dd8ef9f6b0a42)) + # [16.0.0-rc.10](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.9...react-16.0.0-rc.10) (2026-02-19) From bb7fbe8637895adfd5f331bbdbca5e3853d807ef Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:12:51 +0200 Subject: [PATCH 27/46] feat(sidenav): subheading as ReactNode, fix mobile sidenav items padding #421 (#512) * feat(sidenav): subheading as ReactNode, fix mobile sidenav items padding #421 * fix(sidenav): parents are links collapse button positioning inside menu item #421 --- .../sidenav-dropdown/sidenav-dropdown.tsx | 2 +- .../components/sidenav-item/sidenav-item.tsx | 4 +- .../sidenav/{examples.ts => examples.tsx} | 62 +++++++++++++++++++ .../layout/sidenav/sidenav.module.scss | 17 ++++- .../layout/sidenav/sidenav.stories.tsx | 8 +++ 5 files changed, 88 insertions(+), 5 deletions(-) rename src/tedi/components/layout/sidenav/{examples.ts => examples.tsx} (78%) diff --git a/src/tedi/components/layout/sidenav/components/sidenav-dropdown/sidenav-dropdown.tsx b/src/tedi/components/layout/sidenav/components/sidenav-dropdown/sidenav-dropdown.tsx index cce5ef25d..ce1f65895 100644 --- a/src/tedi/components/layout/sidenav/components/sidenav-dropdown/sidenav-dropdown.tsx +++ b/src/tedi/components/layout/sidenav/components/sidenav-dropdown/sidenav-dropdown.tsx @@ -26,7 +26,7 @@ type SideNavDropdownProps = { }; type Group = { - subHeading?: string; + subHeading?: React.ReactNode; subItems: SideNavItemProps[]; }; diff --git a/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.tsx b/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.tsx index 1034845c1..fd6472309 100644 --- a/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.tsx +++ b/src/tedi/components/layout/sidenav/components/sidenav-item/sidenav-item.tsx @@ -23,7 +23,7 @@ export type SideNavItemProps = LinkProps & * Grouped submenu items (preferred for headings) */ subItemGroups?: { - subHeading?: string; + subHeading?: React.ReactNode; subItems: SideNavItemProps[]; }[]; /** @@ -73,6 +73,7 @@ export const SideNavItem = ( const groupsToRender = subItemGroups ?? (subItems ? [{ subItems }] : null); const hasChildren = !!groupsToRender; + const hasTreeIndicator = level > 1 && hasChildren; const SideNavItemBEM = cn( styles['tedi-sidenav__item'], @@ -81,6 +82,7 @@ export const SideNavItem = ( [styles[`tedi-sidenav__item--level-${level}`]]: level > 1, [styles['tedi-sidenav__item--current']]: isActive, [styles['tedi-sidenav__item--has-children']]: hasChildren, + [styles['tedi-sidenav__item--with-tree']]: hasTreeIndicator, }, className ); diff --git a/src/tedi/components/layout/sidenav/examples.ts b/src/tedi/components/layout/sidenav/examples.tsx similarity index 78% rename from src/tedi/components/layout/sidenav/examples.ts rename to src/tedi/components/layout/sidenav/examples.tsx index 152583eff..0bb8ac52e 100644 --- a/src/tedi/components/layout/sidenav/examples.ts +++ b/src/tedi/components/layout/sidenav/examples.tsx @@ -1,4 +1,6 @@ /* istanbul ignore file */ +import { Text } from '../../base/typography/text/text'; +import { InfoButton } from '../../buttons/info-button/info-button'; import { SideNavItemProps } from './components/sidenav-item/sidenav-item'; export const exampleNavItems: SideNavItemProps[] = [ @@ -248,3 +250,63 @@ export const exampleThirdLevelMenuItemsLinks: SideNavItemProps[] = [ { href: '#', children: 'Inventory Management', icon: 'inventory' }, { href: '#', children: 'Billing & Finance', icon: 'payments' }, ]; + +export const exampleThirdLevelMenuItemsLinksWithSubTitles: SideNavItemProps[] = [ + { href: '#', children: 'Dashboard', icon: 'dashboard' }, + { href: '#', children: 'Patient Records', icon: 'people' }, + { + children: 'Clinical Management', + icon: 'medical_services', + subItemGroups: [ + { + subHeading: ( + + Minu tervise ajalugu Lorem ipsum + + ), + subItems: [ + { href: '#', children: 'Active Treatments' }, + { href: '#', children: 'Treatment History' }, + { href: '#', children: 'Treatment Plans' }, + { href: '#', children: 'Clinical Protocols' }, + ], + }, + ], + subItems: [ + { href: '#', children: 'Vital Signs' }, + { href: '#', children: 'Assessments' }, + { + href: '#', + children: 'Treatments', + subItems: [ + { href: '#', children: 'Active Treatments' }, + { href: '#', children: 'Treatment History' }, + { href: '#', children: 'Treatment Plans' }, + { href: '#', children: 'Clinical Protocols' }, + ], + }, + { + href: '#', + children: 'Documentation', + subItems: [ + { href: '#', children: 'Clinical Notes' }, + { href: '#', children: 'Medical Forms' }, + { href: '#', children: 'Consent Forms' }, + { href: '#', children: 'Reports' }, + ], + }, + ], + isDefaultOpen: true, + }, + { + href: '#', + children: 'Administration', + icon: 'admin_panel_settings', + subItems: [ + { href: '#', children: 'Staff Management' }, + { href: '#', children: 'Scheduling' }, + ], + }, + { href: '#', children: 'Inventory Management', icon: 'inventory' }, + { href: '#', children: 'Billing & Finance', icon: 'payments' }, +]; diff --git a/src/tedi/components/layout/sidenav/sidenav.module.scss b/src/tedi/components/layout/sidenav/sidenav.module.scss index 194aa458c..bc48c812f 100644 --- a/src/tedi/components/layout/sidenav/sidenav.module.scss +++ b/src/tedi/components/layout/sidenav/sidenav.module.scss @@ -197,7 +197,8 @@ } } -.tedi-sidenav__item.tedi-sidenav__sub-item .tedi-sidenav__link { +.tedi-sidenav__item--with-tree .tedi-sidenav__item.tedi-sidenav__sub-item .tedi-sidenav__link, +.tedi-sidenav__item--level-2 .tedi-sidenav__link { min-height: var(--navigation-vertical-item-min-height-medium); padding-top: var(--navigation-vertical-item-padding-y); padding-bottom: var(--navigation-vertical-item-padding-y); @@ -208,11 +209,21 @@ ); } +.tedi-sidenav__item:not(.tedi-sidenav__item--with-tree) .tedi-sidenav__sub-item.tedi-sidenav__item .tedi-sidenav__link, +.tedi-sidenav__item--level-2 .tedi-sidenav__link, +.tedi-sidenav__item--with-tree.tedi-sidenav__item--level-2 .tedi-sidenav__link.tedi-sidenav__link--has-children-link { + @include breakpoints.media-breakpoint-down(md) { + padding-left: var(--navigation-vertical-item-padding-right); + } +} + .tedi-sidenav__item .tedi-sidenav__link--has-children-link { padding-right: calc(var(--navigation-vertical-item-padding-right) + var(--button-xs-icon-size)); @include breakpoints.media-breakpoint-down(md) { + z-index: 1; margin-left: calc(var(--navigation-vertical-item-padding-right) + var(--button-xs-icon-size)); + background-color: var(--navigation-vertical-item-background-default); } } @@ -227,7 +238,7 @@ + .tedi-sidenav__link-collapse-wrapper [data-name='collapse-trigger'] { position: absolute; - top: calc(var(--navigation-vertical-item-min-height-medium) / 3); + top: calc((var(--navigation-vertical-item-min-height-large) - var(--button-xs-icon-size)) / 2); right: var(--navigation-vertical-item-padding-right); display: flex; align-items: center; @@ -359,7 +370,7 @@ button.tedi-sidenav__link.tedi-sidenav__back-button { .tedi-sidenav__subheading { padding: var(--layout-grid-gutters-02) var(--navigation-vertical-item-padding-left-level-1) var(--layout-grid-gutters-04) var(--navigation-vertical-item-padding-right); - font-size: var(--size-00); + font-size: var(--body-extra-small-bold-size); color: var(--navigation-vertical-group-title-text); text-transform: uppercase; diff --git a/src/tedi/components/layout/sidenav/sidenav.stories.tsx b/src/tedi/components/layout/sidenav/sidenav.stories.tsx index 0648731f3..30537b06e 100644 --- a/src/tedi/components/layout/sidenav/sidenav.stories.tsx +++ b/src/tedi/components/layout/sidenav/sidenav.stories.tsx @@ -11,6 +11,7 @@ import { exampleNavItems, exampleThirdLevelMenuItems, exampleThirdLevelMenuItemsLinks, + exampleThirdLevelMenuItemsLinksWithSubTitles, } from './examples'; import { SideNav } from './sidenav'; @@ -278,3 +279,10 @@ export const SmallSideNavItems: Story = { sideNavItemSize: 'small', }, }; + +export const SubTitles: Story = { + render: Template, + args: { + navItems: exampleThirdLevelMenuItemsLinksWithSubTitles, + }, +}; From aa8309dc97ceeee06fa0837fe8bf3407ff207f15 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 09:16:39 +0000 Subject: [PATCH 28/46] chore(release): 16.0.0-rc.12 # [16.0.0-rc.12](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.11...react-16.0.0-rc.12) (2026-02-19) ### Features * **sidenav:** subheading as ReactNode, fix mobile sidenav items padding [#421](https://github.com/TEDI-Design-System/react/issues/421) ([#512](https://github.com/TEDI-Design-System/react/issues/512)) ([bb7fbe8](https://github.com/TEDI-Design-System/react/commit/bb7fbe8637895adfd5f331bbdbca5e3853d807ef)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee181fdf..ed3c9df0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.12](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.11...react-16.0.0-rc.12) (2026-02-19) + + +### Features + +* **sidenav:** subheading as ReactNode, fix mobile sidenav items padding [#421](https://github.com/TEDI-Design-System/react/issues/421) ([#512](https://github.com/TEDI-Design-System/react/issues/512)) ([bb7fbe8](https://github.com/TEDI-Design-System/react/commit/bb7fbe8637895adfd5f331bbdbca5e3853d807ef)) + # [16.0.0-rc.11](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.10...react-16.0.0-rc.11) (2026-02-19) From c019e793c5126baba67c516255f3ad3ecfcb2fd7 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:18:10 +0200 Subject: [PATCH 29/46] fix(feedback-text): fix error/success text color variables #517 (#521) --- .../components/form/feedback-text/feedback-text.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tedi/components/form/feedback-text/feedback-text.module.scss b/src/tedi/components/form/feedback-text/feedback-text.module.scss index 0b87caf36..75a9e7877 100644 --- a/src/tedi/components/form/feedback-text/feedback-text.module.scss +++ b/src/tedi/components/form/feedback-text/feedback-text.module.scss @@ -7,11 +7,11 @@ @include mixins.print-grayscale; &--valid { - color: var(--general-status-success-text); + color: var(--form-general-feedback-success-text); } &--error { - color: var(--general-status-danger-text); + color: var(--form-general-feedback-error-text); } &--left { From 43826aefefe8fd14fc821a4eb14f7ace6cf9dcfe Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 09:21:55 +0000 Subject: [PATCH 30/46] chore(release): 16.0.0-rc.13 # [16.0.0-rc.13](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.12...react-16.0.0-rc.13) (2026-02-19) ### Bug Fixes * **feedback-text:** fix error/success text color variables [#517](https://github.com/TEDI-Design-System/react/issues/517) ([#521](https://github.com/TEDI-Design-System/react/issues/521)) ([c019e79](https://github.com/TEDI-Design-System/react/commit/c019e793c5126baba67c516255f3ad3ecfcb2fd7)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed3c9df0d..dfc893a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.13](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.12...react-16.0.0-rc.13) (2026-02-19) + + +### Bug Fixes + +* **feedback-text:** fix error/success text color variables [#517](https://github.com/TEDI-Design-System/react/issues/517) ([#521](https://github.com/TEDI-Design-System/react/issues/521)) ([c019e79](https://github.com/TEDI-Design-System/react/commit/c019e793c5126baba67c516255f3ad3ecfcb2fd7)) + # [16.0.0-rc.12](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.11...react-16.0.0-rc.12) (2026-02-19) From cfeb5f206b9137041b581c610c577e8e600b4fd7 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:23:50 +0200 Subject: [PATCH 31/46] feat(closing-button): add brand and inverted color #6 (#519) * feat(closing-button): add brand and inverted color #6 * fix(closing-button): inherit icon color, fix primary focus color #6 * fix(closing-button): improve prop description #6 --- .../closing-button/closing-button.module.scss | 78 ++++++++++++++----- .../closing-button/closing-button.spec.tsx | 52 ++++++++----- .../closing-button/closing-button.stories.tsx | 38 ++++++++- .../buttons/closing-button/closing-button.tsx | 23 +++++- 4 files changed, 148 insertions(+), 43 deletions(-) diff --git a/src/tedi/components/buttons/closing-button/closing-button.module.scss b/src/tedi/components/buttons/closing-button/closing-button.module.scss index 3004c36a7..628e9b58e 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.module.scss +++ b/src/tedi/components/buttons/closing-button/closing-button.module.scss @@ -1,28 +1,12 @@ .tedi-closing-button { - --general-icon-primary: var(--button-close-text-default); - display: flex; align-items: center; justify-content: center; padding: 0; cursor: pointer; - background-color: var(--button-close-background-default); - border: 1px solid var(--button-close-background-default); + border: 0; border-radius: var(--button-radius-default); - transition: background 0.5s ease; - - @each $state in hover, active { - &:#{ $state } { - --general-icon-primary: var(--button-close-text-#{$state}); - - background-color: var(--button-close-background-#{$state}); - } - } - - &:focus-visible { - outline: 2px solid var(--button-main-primary-border-focus); - outline-offset: -2px; - } + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease; &--default { width: var(--button-sm-icon-size); @@ -33,4 +17,62 @@ width: var(--button-xs-icon-size); height: var(--button-xs-icon-size); } + + &--color-primary { + color: var(--button-close-text-default); + background-color: var(--button-close-background-default); + + &:hover { + color: var(--button-close-text-hover); + background-color: var(--button-close-background-hover); + } + + &:active { + color: var(--button-close-text-active); + background-color: var(--button-close-background-active); + } + + &:focus-visible { + color: var(--button-close-text-focus); + outline: 2px solid var(--button-main-primary-border-focus); + } + } + + &--color-brand { + color: var(--button-main-neutral-text-default); + background-color: var(--button-main-neutral-icon-only-background-default); + + &:hover { + color: var(--button-main-neutral-text-hover); + background-color: var(--button-main-neutral-icon-only-background-hover); + } + + &:active { + color: var(--button-main-neutral-text-active); + background-color: var(--button-main-neutral-icon-only-background-active); + } + + &:focus-visible { + outline: 2px solid var(--button-main-primary-border-focus); + } + } + + &--color-white { + color: var(--button-neutral-inverted-text-default); + background-color: var(--button-neutral-inverted-icon-only-background-default); + + &:hover { + color: var(--button-neutral-inverted-text-hover); + background-color: var(--button-main-neutral-inverted-icon-only-background-hover); + } + + &:active { + color: var(--button-neutral-inverted-text-active); + background-color: var(--button-main-neutral-inverted-icon-only-background-hover); + } + + &:focus-visible { + outline: 2px solid var(--button-main-neutral-inverted-text-focus); + } + } } diff --git a/src/tedi/components/buttons/closing-button/closing-button.spec.tsx b/src/tedi/components/buttons/closing-button/closing-button.spec.tsx index ff51a20ef..9e27d065a 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.spec.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.spec.tsx @@ -10,33 +10,35 @@ jest.mock('../../../providers/label-provider', () => ({ describe('ClosingButton component', () => { it('renders the ClosingButton with default props', () => { - render( - { - console.log('Button pressed'); - }} - /> - ); + render(); const button = screen.getByRole('button', { name: /close/i }); expect(button).toBeInTheDocument(); - expect(button).toHaveClass('tedi-closing-button'); - expect(button).toHaveClass('tedi-closing-button--default'); + expect(button).toHaveClass('tedi-closing-button tedi-closing-button--default tedi-closing-button--color-primary'); const icon = button.querySelector('span[data-name="icon"]'); expect(icon).toBeInTheDocument(); expect(icon).toHaveClass('tedi-icon--size-24'); }); - it('renders with small size when size="small"', () => { + it('renders with the correct small size class and icon size', () => { render(); const button = screen.getByRole('button', { name: /close/i }); + expect(button).toHaveClass('tedi-closing-button tedi-closing-button--small tedi-closing-button--color-primary'); + render( + { + console.log('Button pressed'); + }} + /> + ); + + expect(button).toBeInTheDocument(); + expect(button).toHaveClass('tedi-closing-button'); expect(button).toHaveClass('tedi-closing-button--small'); const icon = button.querySelector('span[data-name="icon"]'); - expect(icon).toBeInTheDocument(); - // Small button still uses default icon size expect(icon).toHaveClass('tedi-icon--size-24'); }); @@ -54,17 +56,21 @@ describe('ClosingButton component', () => { it('applies custom class names', () => { render(); - const button = screen.getByRole('button', { name: /Close/i }); - expect(button).toHaveClass('tedi-closing-button'); - expect(button).toHaveClass('tedi-closing-button--default'); - expect(button).toHaveClass('custom-class'); + const button = screen.getByRole('button', { name: /close/i }); + expect(button).toHaveClass( + 'tedi-closing-button tedi-closing-button--default tedi-closing-button--color-primary custom-class' + ); + const customButton = screen.getByRole('button', { name: /Close/i }); + expect(customButton).toHaveClass('tedi-closing-button'); + expect(customButton).toHaveClass('tedi-closing-button--default'); + expect(customButton).toHaveClass('custom-class'); }); it('triggers onClick handler when clicked', () => { const onClickMock = jest.fn(); render(); - const button = screen.getByRole('button', { name: /Close/i }); + const button = screen.getByRole('button', { name: /close/i }); fireEvent.click(button); expect(onClickMock).toHaveBeenCalledTimes(1); }); @@ -72,7 +78,7 @@ describe('ClosingButton component', () => { it('uses fallback label from label provider when title is not provided', () => { render(); - const button = screen.getByRole('button', { name: /Close/i }); + const button = screen.getByRole('button', { name: /close/i }); expect(button).toHaveAttribute('title', 'Close'); expect(button).toHaveAttribute('aria-label', 'Close'); }); @@ -84,4 +90,14 @@ describe('ClosingButton component', () => { expect(button).toHaveAttribute('title', 'Custom Close'); expect(button).toHaveAttribute('aria-label', 'Custom Close'); }); + + it('applies the correct color classes', () => { + const colors: Array<'primary' | 'brand' | 'white'> = ['primary', 'brand', 'white']; + + colors.forEach((color) => { + const { container } = render(); + const button = container.querySelector('button[data-name="closing-button"]'); + expect(button).toHaveClass(`tedi-closing-button--color-${color}`); + }); + }); }); diff --git a/src/tedi/components/buttons/closing-button/closing-button.stories.tsx b/src/tedi/components/buttons/closing-button/closing-button.stories.tsx index 2055e2d08..d2e0a084b 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.stories.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; +import { Text } from '../../base/typography/text/text'; import { Col, Row } from '../../layout/grid'; import ClosingButton, { ClosingButtonProps } from './closing-button'; @@ -42,16 +43,18 @@ const SizeTemplate: StoryFn = () => { const stateArray = ['Default', 'Hover', 'Active', 'Focus']; -const StatesTemplate: StoryFn = () => { +const StatesTemplate: StoryFn = (args) => { return (
{stateArray.map((state) => ( - {state} + + {state} + - + ))} @@ -116,3 +119,32 @@ export const States: Story = { }, }, }; + +export const ColorBrand: Story = { + render: StatesTemplate, + args: { + color: 'brand', + }, + parameters: { + pseudo: { + hover: '#Hover', + active: '#Active', + focusVisible: '#Focus', + }, + }, +}; + +export const ColorInverted: Story = { + render: StatesTemplate, + args: { + color: 'white', + }, + parameters: { + pseudo: { + hover: '#Hover', + active: '#Active', + focusVisible: '#Focus', + }, + backgrounds: { default: 'brand' }, + }, +}; diff --git a/src/tedi/components/buttons/closing-button/closing-button.tsx b/src/tedi/components/buttons/closing-button/closing-button.tsx index 4e9f60d87..0eb7c77a5 100644 --- a/src/tedi/components/buttons/closing-button/closing-button.tsx +++ b/src/tedi/components/buttons/closing-button/closing-button.tsx @@ -6,6 +6,7 @@ import { useLabels } from '../../../providers/label-provider'; import { Icon } from '../../base/icon/icon'; import styles from './closing-button.module.scss'; +type ClosingButtonColor = 'primary' | 'brand' | 'white'; export type ClosingButtonSize = 'default' | 'small'; export type ClosingButtonIconSize = 18 | 24; @@ -16,7 +17,7 @@ export interface ClosingButtonProps extends ButtonHTMLAttributes { const { getLabel } = useLabels(); - const { title = getLabel('close'), onClick, size = 'default', iconSize = 24, className, ...rest } = props; + const { + title = getLabel('close'), + onClick, + size = 'default', + iconSize = 24, + color = 'primary', + className, + ...rest + } = props; const resolvedSize: ClosingButtonSize = iconSize === 18 ? 'small' : size; const buttonClass = cn( styles['tedi-closing-button'], { + [styles[`tedi-closing-button--${size}`]]: size, + [styles[`tedi-closing-button--color-${color}`]]: color, [styles[`tedi-closing-button--${resolvedSize}`]]: resolvedSize, }, className @@ -62,7 +77,7 @@ export const ClosingButton = (props: ClosingButtonProps): JSX.Element => { title={title} aria-label={title} > - + ); }; From 24be799cf6a6adb9b71210836f21fbd4c8f1aa7f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 19 Feb 2026 09:27:48 +0000 Subject: [PATCH 32/46] chore(release): 16.0.0-rc.14 # [16.0.0-rc.14](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.13...react-16.0.0-rc.14) (2026-02-19) ### Features * **closing-button:** add brand and inverted color [#6](https://github.com/TEDI-Design-System/react/issues/6) ([#519](https://github.com/TEDI-Design-System/react/issues/519)) ([cfeb5f2](https://github.com/TEDI-Design-System/react/commit/cfeb5f206b9137041b581c610c577e8e600b4fd7)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc893a30..8257189b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.14](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.13...react-16.0.0-rc.14) (2026-02-19) + + +### Features + +* **closing-button:** add brand and inverted color [#6](https://github.com/TEDI-Design-System/react/issues/6) ([#519](https://github.com/TEDI-Design-System/react/issues/519)) ([cfeb5f2](https://github.com/TEDI-Design-System/react/commit/cfeb5f206b9137041b581c610c577e8e600b4fd7)) + # [16.0.0-rc.13](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.12...react-16.0.0-rc.13) (2026-02-19) From 455c491a753dbfc35d7c996fb94aa15639bbdc51 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:59:53 +0200 Subject: [PATCH 33/46] fix(choice-group): segmented item borders and focus behaviour #101 (#513) --- .../form/choice-group/choice-group.tsx | 6 +- .../choice-group-item.module.scss | 80 ++++++++++++++----- .../choice-group-item/choice-group-item.tsx | 11 ++- 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/tedi/components/form/choice-group/choice-group.tsx b/src/tedi/components/form/choice-group/choice-group.tsx index e2ade8d04..e3856bba7 100644 --- a/src/tedi/components/form/choice-group/choice-group.tsx +++ b/src/tedi/components/form/choice-group/choice-group.tsx @@ -67,7 +67,7 @@ export const ChoiceGroup = (props: ChoiceGroupProps): React.ReactElement => { indeterminateCheck, indeterminateCheckProps = {}, color, - layout, + layout = 'separated', showIndicator, ...rest } = getCurrentBreakpointProps(props); @@ -188,8 +188,8 @@ export const ChoiceGroup = (props: ChoiceGroupProps): React.ReactElement => { )} -
+
{variant === 'default' || showIndicator ? ( { onChangeHandler(value, e.target.checked); }} - className={styles['tedi-choice-group-item__input']} + className="visually-hidden" role={type === 'radio' ? 'radio' : undefined} aria-checked={isChecked} + tabIndex={-1} />
- {helper && } + {helper && } {inputUpdated && (
{inputUpdated} From 26ca01b7c576caf898872d53f1012f6763ed5341 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 20 Feb 2026 05:26:31 +0000 Subject: [PATCH 37/46] chore(release): 16.0.0-rc.16 # [16.0.0-rc.16](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.15...react-16.0.0-rc.16) (2026-02-20) ### Bug Fixes * **number-field:** fix variables [#516](https://github.com/TEDI-Design-System/react/issues/516) ([#522](https://github.com/TEDI-Design-System/react/issues/522)) ([dcf6240](https://github.com/TEDI-Design-System/react/commit/dcf62404afa9543c13277623a9aaa6daed1b93df)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5839a7c27..49907ce25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.16](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.15...react-16.0.0-rc.16) (2026-02-20) + + +### Bug Fixes + +* **number-field:** fix variables [#516](https://github.com/TEDI-Design-System/react/issues/516) ([#522](https://github.com/TEDI-Design-System/react/issues/522)) ([dcf6240](https://github.com/TEDI-Design-System/react/commit/dcf62404afa9543c13277623a9aaa6daed1b93df)) + # [16.0.0-rc.15](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.14...react-16.0.0-rc.15) (2026-02-19) From 097627db68e23afaa27dd288319b34ccba3e5888 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:05:43 +0200 Subject: [PATCH 38/46] fix(checkbox,radio): show pointer on label hover #532 (#533) --- src/tedi/components/form/checkbox/checkbox.module.scss | 2 +- src/tedi/components/form/checkbox/checkbox.tsx | 6 +++++- src/tedi/components/form/radio/radio.module.scss | 3 ++- src/tedi/components/form/radio/radio.tsx | 6 +++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tedi/components/form/checkbox/checkbox.module.scss b/src/tedi/components/form/checkbox/checkbox.module.scss index 20e351cd3..b7562273b 100644 --- a/src/tedi/components/form/checkbox/checkbox.module.scss +++ b/src/tedi/components/form/checkbox/checkbox.module.scss @@ -6,7 +6,7 @@ @include mixins.print-grayscale; - &:hover:not(.checkbox--disabled) { + &:hover:not(.tedi-checkbox--disabled) label { cursor: pointer; } diff --git a/src/tedi/components/form/checkbox/checkbox.tsx b/src/tedi/components/form/checkbox/checkbox.tsx index 8a0d87354..303f766db 100644 --- a/src/tedi/components/form/checkbox/checkbox.tsx +++ b/src/tedi/components/form/checkbox/checkbox.tsx @@ -57,7 +57,11 @@ export const Checkbox = (props: CheckboxProps): JSX.Element => { const LabelBEM = cn(styles['tedi-checkbox__label'], { [styles['tedi-checkbox--disabled']]: disabled }); return ( -
+
diff --git a/src/tedi/components/form/radio/radio.module.scss b/src/tedi/components/form/radio/radio.module.scss index 4c4aa6cc4..bca07f7dc 100644 --- a/src/tedi/components/form/radio/radio.module.scss +++ b/src/tedi/components/form/radio/radio.module.scss @@ -6,12 +6,13 @@ @include mixins.print-grayscale; - &:hover:not(.tedi-radio--disabled) { + &:hover:not(.tedi-radio--disabled) label { cursor: pointer; } &--disabled { color: var(--general-text-disabled); + cursor: not-allowed; } &__input { diff --git a/src/tedi/components/form/radio/radio.tsx b/src/tedi/components/form/radio/radio.tsx index 9590b8ff4..d49a6cda6 100644 --- a/src/tedi/components/form/radio/radio.tsx +++ b/src/tedi/components/form/radio/radio.tsx @@ -50,7 +50,11 @@ export const Radio = (props: RadioProps): JSX.Element => { const LabelBEM = cn(styles['tedi-radio__label'], { [styles['tedi-radio--disabled']]: disabled }); return ( -
+
From 043a23618e9e5f3b716b1ddb776f14982ad6ba08 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 25 Feb 2026 13:09:57 +0000 Subject: [PATCH 39/46] chore(release): 16.0.0-rc.17 # [16.0.0-rc.17](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.16...react-16.0.0-rc.17) (2026-02-25) ### Bug Fixes * **checkbox,radio:** show pointer on label hover [#532](https://github.com/TEDI-Design-System/react/issues/532) ([#533](https://github.com/TEDI-Design-System/react/issues/533)) ([097627d](https://github.com/TEDI-Design-System/react/commit/097627db68e23afaa27dd288319b34ccba3e5888)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49907ce25..772692d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.17](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.16...react-16.0.0-rc.17) (2026-02-25) + + +### Bug Fixes + +* **checkbox,radio:** show pointer on label hover [#532](https://github.com/TEDI-Design-System/react/issues/532) ([#533](https://github.com/TEDI-Design-System/react/issues/533)) ([097627d](https://github.com/TEDI-Design-System/react/commit/097627db68e23afaa27dd288319b34ccba3e5888)) + # [16.0.0-rc.16](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.15...react-16.0.0-rc.16) (2026-02-20) From 93e9e118aab73d40347029089c5370851a7b551c Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:32:16 +0200 Subject: [PATCH 40/46] feat(separator): redesign with new spacing API, dotPosition and dotStyle support #30 (#525) * feat(separator): redesign with new spacing API, dotPosition and dotStyle support #30 BREAKING CHANGE: legacy spacing props removed, dotSize enum changed, variant values simplified * chore: fix stories that use Separator #30 * fix(separator): design review fixes #30 * fix(separator): change outlined dotted dot background color #30 * fix(separator): improve css #30 --- .../left-panel/left-panel-footer.tsx | 2 +- .../components/cards/card/card.stories.tsx | 2 +- .../misc/separator/separator.module.scss | 244 ++++++---- .../misc/separator/separator.spec.tsx | 177 +++---- .../misc/separator/separator.stories.tsx | 441 +++++++++++++----- .../components/misc/separator/separator.tsx | 186 +++++--- 6 files changed, 679 insertions(+), 373 deletions(-) diff --git a/src/community/components/map-components/left-panel/left-panel-footer.tsx b/src/community/components/map-components/left-panel/left-panel-footer.tsx index 526f38fc9..f3d725cbc 100644 --- a/src/community/components/map-components/left-panel/left-panel-footer.tsx +++ b/src/community/components/map-components/left-panel/left-panel-footer.tsx @@ -7,7 +7,7 @@ const LeftPanelFooter = () => {
Tume režiim} /> - +
diff --git a/src/tedi/components/cards/card/card.stories.tsx b/src/tedi/components/cards/card/card.stories.tsx index 4d2c39ab7..48ddc9f8e 100644 --- a/src/tedi/components/cards/card/card.stories.tsx +++ b/src/tedi/components/cards/card/card.stories.tsx @@ -283,7 +283,7 @@ const Timeline: StoryFn = (args) => (

Card content

- +

Card content

diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index 0e17b456e..6d648458d 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -13,6 +13,15 @@ $sizes: ( '2-5': 2.5rem, '5': 5rem, ); +$thicknesses: ( + '1': 1px, + '2': 2px, +); +$dot-colors: ( + 'primary': var(--general-border-primary), + 'secondary': var(--general-border-secondary), + 'accent': var(--general-border-accent), +); .tedi-separator { --vertical-separator-height: 100%; @@ -30,12 +39,8 @@ $sizes: ( border-left: 1px solid var(--general-border-primary); } - &--secondary { - border-color: var(--general-border-secondary); - } - - &--accent { - border-color: var(--general-border-accent); + &--horizontal { + display: block; } &--block { @@ -45,84 +50,38 @@ $sizes: ( &--inline { display: inline; } -} - -.tedi-separator--dotted, -.tedi-separator--dotted-small { - &::before { - position: absolute; - top: 1.25rem; - width: var(--separator-dotted-dot-lg); - height: var(--separator-dotted-dot-lg); - content: ''; - background-color: var(--general-border-primary); - border-radius: 100%; - transform: translateX(-8px); - - @include mixins.print-grayscale; - } - &.tedi-separator--secondary::before { - background-color: var(--general-border-secondary); + &--secondary { + border-color: var(--general-border-secondary); } - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); + &--accent { + border-color: var(--general-border-accent); } -} -.tedi-separator--dotted-small::before { - top: 1.5rem; - width: var(--separator-dotted-dot-md); - height: var(--separator-dotted-dot-md); - transform: translateX(-5px); -} - -.tedi-separator--is-stretched { - margin-right: calc(var(--card-content-padding-right) * -1); - margin-left: calc(var(--card-content-padding-left) * -1); + &--is-stretched { + margin-right: calc(var(--card-content-padding-right) * -1); + margin-left: calc(var(--card-content-padding-left) * -1); - &.tedi-separator--vertical { - height: calc(100% + (var(--card-content-padding-top) + var(--card-content-padding-bottom))); - margin: calc(var(--card-content-padding-top) * -1) 0 calc(var(--card-content-padding-bottom) * -1); + &.tedi-separator--vertical { + height: calc(100% + (var(--card-content-padding-top) + var(--card-content-padding-bottom))); + margin: calc(var(--card-content-padding-top) * -1) 0 calc(var(--card-content-padding-bottom) * -1); + } } } -@each $size, $value in $sizes { - .tedi-separator--horizontal.tedi-separator--top-#{$size} { - margin-top: #{$value}; - } - - .tedi-separator--horizontal.tedi-separator--bottom-#{$size} { - margin-bottom: #{$value}; - } - - .tedi-separator--horizontal.tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { - margin-top: #{$value}; - margin-bottom: #{$value}; - } +.tedi-separator--dotted { + &::before { + position: absolute; + content: ''; + background-color: var(--general-border-primary); + border-radius: 50%; - .tedi-separator--horizontal.tedi-separator--dot-only.tedi-separator--spacing-#{$size}, - .tedi-separator--vertical.tedi-separator--spacing-#{$size} { - margin-right: #{$value}; - margin-left: #{$value}; + @include mixins.print-grayscale; } -} - -$thicknesses: ( - '1': 1px, - '2': 2px, -); -@each $thickness, $value in $thicknesses { - .tedi-separator { - &.tedi-separator--thickness-#{$thickness} { - border-top-width: #{$value}; - } - - &--vertical.tedi-separator--thickness-#{$thickness} { - border-left-width: #{$value}; - } + &.tedi-separator--vertical { + min-height: 3rem; } } @@ -138,58 +97,139 @@ $thicknesses: ( width: var(--separator-dotted-dot-sm); height: var(--separator-dotted-dot-sm); content: ''; - border-radius: 100%; + border-radius: 50%; @include mixins.print-grayscale; } - &.tedi-separator--dot-only-extra-small::before { - width: var(--separator-dotted-dot-xs); - height: var(--separator-dotted-dot-xs); + &.tedi-separator--dot-style-outlined::before { + background: transparent; } +} - &.tedi-separator--dot-only-small::before { - width: var(--separator-dotted-dot-sm); - height: var(--separator-dotted-dot-sm); +@each $name, $color in $dot-colors { + .tedi-separator--#{$name} { + border-color: $color; + + &::before { + background-color: $color; + } + + &.tedi-separator--dot-style-outlined:not(.tedi-separator--dot-only)::before { + border-color: $color; + } } +} - &.tedi-separator--dot-only-medium::before { - width: var(--separator-dotted-dot-md); - height: var(--separator-dotted-dot-md); +@each $size, $var in (extra-small: xs, small: sm, medium: md, large: lg) { + .tedi-separator--dot-only-#{$size}::before, + .tedi-separator--dotted-#{$size}::before, + .tedi-separator--dot-style-outlined.tedi-separator--dotted-#{$size}::before { + width: var(--separator-dot-size-#{$var}); + height: var(--separator-dot-size-#{$var}); } +} - &.tedi-separator--dot-only-large::before { - width: var(--separator-dotted-dot-lg); - height: var(--separator-dotted-dot-lg); +@each $axis in (horizontal, vertical) { + @each $pos in (start, center, end) { + .tedi-separator--#{$axis}.tedi-separator--dot-position-#{$pos}::before { + @if $axis == horizontal { + @if $pos == start { + right: auto; + left: 0; + } @else if $pos == center { + right: 0; + left: 0; + margin: auto; + } @else { + right: 0; + left: auto; + } + } @else { + @if $pos == start { + top: 0; + bottom: auto; + } @else if $pos == center { + top: 0; + bottom: 0; + margin: auto 0; + } @else { + top: auto; + bottom: 0; + } + } + } } +} - &.tedi-separator--primary::before { - background-color: var(--general-border-primary); +.tedi-separator--horizontal.tedi-separator--dot-position-custom::before { + right: auto; + left: var(--separator-dot-position); +} + +.tedi-separator--vertical.tedi-separator--dot-position-custom::before { + top: var(--separator-dot-position); + bottom: auto; +} + +@each $size, $value in $sizes { + .tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { + margin: #{$value} 0; + } + + .tedi-separator--dot-only.tedi-separator--spacing-#{$size} { + &.tedi-separator--horizontal { + margin-right: $value; + margin-left: $value; + } + + &.tedi-separator--vertical { + margin-top: $value; + margin-bottom: $value; + } } - &.tedi-separator--secondary::before { - background-color: var(--general-border-secondary); + @each $side in (top, bottom, left, right) { + .tedi-separator--#{$side}-#{$size} { + margin-#{$side}: $value; + } } +} - &.tedi-separator--accent::before { - background-color: var(--general-border-accent); +@each $thickness, $value in $thicknesses { + .tedi-separator--thickness-#{$thickness} { + border-top-width: $value; + } + .tedi-separator--vertical.tedi-separator--thickness-#{$thickness} { + border-left-width: $value; } } -.tedi-separator--horizontal { - &.tedi-separator--dotted::before, - &.tedi-separator--dotted-small::before { - right: 0; - left: 0; - margin: 0 auto; - transform: initial; +@each $axis in (horizontal, vertical) { + @each $size, $var in (extra-small: xs, small: sm, medium: md, large: lg) { + .tedi-separator--#{$axis}.tedi-separator--dotted-#{$size}::before, + .tedi-separator--#{$axis}.tedi-separator--dot-style-outlined.tedi-separator--dotted-#{$size}::before { + @if $axis == horizontal { + top: calc((var(--separator-dot-size-#{$var}) / 2 + var(--separator-thickness)) * -1); + } @else { + left: calc((var(--separator-dot-size-#{$var}) / 2 + var(--separator-thickness) / 2) * -1); + } + } } +} - &.tedi-separator--dotted::before { - top: calc(var(--separator-dotted-dot-lg) / 2 * -1); +.tedi-separator--dot-style-outlined { + &::before { + background-color: var(--general-surface-primary); + border-style: solid; + border-width: var(--separator-thickness); } - &.tedi-separator--dotted-small::before { - top: calc(var(--separator-dotted-dot-md) / 2 * -1); + @each $name, $color in $dot-colors { + &.tedi-separator--#{$name}::before, + &.tedi-separator--dotted-#{$name}::before { + z-index: 10; + border-color: $color; + } } } diff --git a/src/tedi/components/misc/separator/separator.spec.tsx b/src/tedi/components/misc/separator/separator.spec.tsx index 65d9c7e20..694961d9a 100644 --- a/src/tedi/components/misc/separator/separator.spec.tsx +++ b/src/tedi/components/misc/separator/separator.spec.tsx @@ -17,132 +17,141 @@ describe('Separator Component', () => { }); }); - const renderComponent = (props: SeparatorProps) => render(); + const renderComponent =

(props: P) => + render(); - it('should render a div element by default', () => { - const { getByTestId } = renderComponent({}); + it('renders a div by default', () => { + const { getByTestId } = renderComponent({ element: 'div' }); expect(getByTestId('separator').tagName).toBe('DIV'); }); - it('should render the specified element', () => { + it('renders the specified element', () => { const { getByTestId } = renderComponent({ element: 'hr' }); expect(getByTestId('separator').tagName).toBe('HR'); }); - it('should apply default classes', () => { - const { getByTestId } = renderComponent({}); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator']); - expect(separator).toHaveClass(styles['tedi-separator--thickness-1']); - expect(separator).toHaveClass(styles['tedi-separator--horizontal']); - }); - - it('should apply additional classes from className prop', () => { - const { getByTestId } = renderComponent({ className: 'custom-class' }); - expect(getByTestId('separator')).toHaveClass('custom-class'); - }); - - it('should apply correct spacing class', () => { - const { getByTestId } = renderComponent({ spacing: 1.25 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--spacing-1-25']); - }); + it('applies base and default classes', () => { + const { getByTestId } = renderComponent({ axis: 'horizontal' }); + const el = getByTestId('separator'); - it('should apply correct top and bottom spacing classes', () => { - const { getByTestId } = renderComponent({ topSpacing: 0.5, bottomSpacing: 1 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--top-0-5']); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--bottom-1']); + expect(el).toHaveClass(styles['tedi-separator']); + expect(el).toHaveClass(styles['tedi-separator--horizontal']); + expect(el).toHaveClass(styles['tedi-separator--thickness-1']); }); - it('should apply axis specific class', () => { - const { getByTestId } = renderComponent({ axis: 'vertical' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--vertical']); + it('applies custom className', () => { + const { getByTestId } = renderComponent({ className: 'my-extra-class' }); + expect(getByTestId('separator')).toHaveClass('my-extra-class'); }); - it('should apply color class', () => { + it('applies color modifier', () => { const { getByTestId } = renderComponent({ color: 'accent' }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--accent']); }); - it('should apply variant class', () => { - const { getByTestId } = renderComponent({ variant: 'dotted' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dotted']); + it('applies vertical axis class and allows height', () => { + const { getByTestId } = renderComponent({ axis: 'vertical', height: 5.5 }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--vertical']); + expect(el).toHaveStyle('--vertical-separator-height: 5.5rem'); }); - it('should apply thickness class when no variant is used', () => { - const { getByTestId } = renderComponent({ thickness: 2 }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--thickness-2']); + it('applies spacing classes — number value', () => { + const { getByTestId } = renderComponent({ spacing: 1.5 }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--top-1-5']); + expect(el).toHaveClass(styles['tedi-separator--bottom-1-5']); }); - it('should not apply thickness class when variant is used', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', thickness: 2 }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--thickness-2']); + it('applies spacing classes — object value', () => { + const { getByTestId } = renderComponent({ + spacing: { top: 2, bottom: 0.5, left: 1 }, + axis: 'horizontal', + }); + const el = getByTestId('separator'); + + expect(el).toHaveClass(styles['tedi-separator--top-2']); + expect(el).toHaveClass(styles['tedi-separator--bottom-0-5']); + expect(el).toHaveClass(styles['tedi-separator--left-1']); }); - it('should apply isStretched class', () => { + it('applies isStretched class', () => { const { getByTestId } = renderComponent({ isStretched: true }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--is-stretched']); }); - it('should set height CSS variable for vertical separator', () => { - const { getByTestId } = renderComponent({ axis: 'vertical', height: 2 }); - expect(getByTestId('separator')).toHaveStyle('--vertical-separator-height: 2rem'); + it('applies thickness class when no variant', () => { + const { getByTestId } = renderComponent({ thickness: 2 }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--thickness-2']); }); + it('applies dotted class and dot size', () => { + const { getByTestId } = renderComponent({ + variant: 'dotted', + dotSize: 'small', + dotPosition: 'center', + }); + const el = getByTestId('separator'); - it('should apply dot-only variant class', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only']); + expect(el).toHaveClass(styles['tedi-separator--dotted']); + expect(el).toHaveClass(styles['tedi-separator--dotted-small']); + expect(el).toHaveClass(styles['tedi-separator--dot-position-center']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is extra small', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'extra-small' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-extra-small']); + it('applies custom dot position via CSS var', () => { + const { getByTestId } = renderComponent({ + variant: 'dotted', + dotPosition: 2.75, + }); + expect(getByTestId('separator')).toHaveStyle('--separator-dot-position: 2.75rem'); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-position-custom']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is small', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'small' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-small']); - }); + it('applies dot-only and dot size class', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'large', + dotStyle: 'filled', + }); + const el = getByTestId('separator'); - it('should apply correct dot size class when variant is dot-only and dotSize is medium', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'medium' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-medium']); + expect(el).toHaveClass(styles['tedi-separator--dot-only']); + expect(el).toHaveClass(styles['tedi-separator--dot-only-large']); + expect(el).not.toHaveClass(styles['tedi-separator--dot-style-outlined']); }); - it('should apply correct dot size class when variant is dot-only and dotSize is large', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'large' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-large']); - }); + it('applies outlined style and thickness var when outlined', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'medium', + dotStyle: 'outlined', + thickness: 2, + }); + const el = getByTestId('separator'); - it('should not apply dot size class when variant is not dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', dotSize: 'large' }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--dot-only-large']); + expect(el).toHaveClass(styles['tedi-separator--dot-style-outlined']); + expect(el).toHaveStyle('--separator-thickness: 2px'); + expect(el).toHaveClass(styles['tedi-separator--thickness-2']); }); - it('should default vertical separator display to block', () => { + it('requires dotSize (but test fallback/default)', () => { + const { getByTestId } = renderComponent({ + variant: 'dot-only', + dotSize: 'large', + }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--dot-only-large']); + }); + it('defaults to block display in vertical mode', () => { const { getByTestId } = renderComponent({ axis: 'vertical' }); expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--block']); }); - it('should apply inline display class to vertical separator when specified', () => { - const { getByTestId } = renderComponent({ axis: 'vertical', display: 'inline' }); - expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--inline']); - }); - - it('should not apply inline display class to horizontal separator', () => { - const { getByTestId } = renderComponent({ axis: 'horizontal', display: 'block' }); - expect(getByTestId('separator')).not.toHaveClass(styles['tedi-separator--inline']); - }); - it('should apply dotSize class when variant is dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dot-only', dotSize: 'medium' }); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator--dot-only']); - expect(separator).toHaveClass(styles['tedi-separator--dot-only-medium']); - }); - - it('should ignore dotSize when variant is not dot-only', () => { - const { getByTestId } = renderComponent({ variant: 'dotted', dotSize: 'medium' }); - const separator = getByTestId('separator'); - expect(separator).toHaveClass(styles['tedi-separator--dotted']); - expect(separator).not.toHaveClass(styles['tedi-separator--dot-only-medium']); + it('applies inline display when specified (vertical)', () => { + const { getByTestId } = renderComponent({ + axis: 'vertical', + display: 'inline-block', + }); + expect(getByTestId('separator')).toHaveClass(styles['tedi-separator--inline-block']); }); }); diff --git a/src/tedi/components/misc/separator/separator.stories.tsx b/src/tedi/components/misc/separator/separator.stories.tsx index 5d5a4f6f5..385b2a01d 100644 --- a/src/tedi/components/misc/separator/separator.stories.tsx +++ b/src/tedi/components/misc/separator/separator.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryFn, StoryObj } from '@storybook/react'; -import { Fragment } from 'react/jsx-runtime'; import { Text } from '../../base/typography/text/text'; import { Card, CardContent } from '../../cards/card'; import { Col, Row } from '../../layout/grid'; import { VerticalSpacing } from '../../layout/vertical-spacing'; -import Separator, { SeparatorProps } from './separator'; +import Separator, { DotSize, SeparatorProps } from './separator'; /** * Figma ↗
@@ -32,86 +31,254 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const colorArray: SeparatorProps['color'][] = ['primary', 'secondary', 'accent']; +const spacingArray: SeparatorProps['spacing'][] = [0, 0.5, 1, 1.5, 2, 2.5]; +const sizeArray: SeparatorProps['dotSize'][] = ['large', 'medium']; +type TemplateMultipleProps = SeparatorProps & { + array: Type[]; + property: keyof SeparatorProps; +}; +const Template: StoryFn = (args) => ; -const Template: StoryFn = (args) => ( - <> - Some content - - Other content - -); +const SizesTemplate: StoryFn = (args) => { + const { array } = args; -const ColorsAndThickness: StoryFn = (args) => ( - <> - {colorArray.map((color) => ( - - - - + return ( +

+ {array.map((value, key) => ( + + + {value === 'large' ? 'Large' : 'Medium'} - - - - + + + + + - - ))} - -); + ))} +
+ ); +}; -const VerticalColorTemplate: StoryFn = (args) => ( +const ColorsAndThickness: StoryFn = (args) => ( - {colorArray.map((color) => ( - - - - - - - - - - - ))} + + + + ); -const DotOnlyTemplate: StoryFn = (args) => ( +const SpacingHorizontal: StoryFn = (args) => ( - - - - + {spacingArray.map((spacing, index) => ( + + ))} ); +const SpacingVertical: StoryFn = (args) => ( + + {spacingArray.map((spacing, index) => ( + + + + ))} + +); + export const Default: Story = { render: Template, args: { spacing: 1 }, }; -export const HorizontalColors: Story = { +export const HorizontalSpacings: Story = { + render: SpacingHorizontal, + args: { + axis: 'horizontal', + }, +}; + +export const HorizontalThickness: Story = { render: ColorsAndThickness, - args: { spacing: 1 }, }; -export const VerticalColors: Story = { - render: VerticalColorTemplate, - args: { axis: 'vertical', height: 5 }, +export const Vertical: Story = { + render: Template, + args: { axis: 'vertical', height: 3 }, +}; + +export const VerticalSpacings: Story = { + render: SpacingVertical, + args: { + axis: 'vertical', + height: 3, + display: 'inline-block', + }, +}; + +export const VerticalThickness: Story = { + render: ColorsAndThickness, + args: { axis: 'vertical', height: 3, display: 'inline' }, }; -export const PaddedEven: Story = { +export const DottedLineHorizontal: Story = { render: Template, - args: { spacing: 1 }, + args: { axis: 'horizontal', variant: 'dotted', color: 'accent', dotPosition: 'center' }, }; -export const PaddedUneven: Story = { +export const DottedLineVertical: Story = { render: Template, - args: { topSpacing: 2.5, bottomSpacing: 0.5 }, + args: { axis: 'vertical', variant: 'dotted', color: 'accent', height: 5, dotPosition: 'center' }, +}; + +export const Sizes: StoryObj = { + render: SizesTemplate, + + args: { + property: 'dotSize', + array: sizeArray, + }, +}; + +export const SpacingTopDefault: Story = { + render: () => { + return ( + + + + + + + + + ); + }, +}; + +export const SpacingTopSmall: Story = { + render: () => { + return ( + + + + + + + + + ); + }, +}; + +export const Position: Story = { + render: () => { + return ( + <> + + + + Start + + +
+ +
+ + + + +
+
+ + + + Center + + + + + + + + + + + + + End + + + + + + + + + + +
+ + ); + }, }; const TemplateVertical: StoryFn = (args) => ( @@ -137,88 +304,132 @@ const TemplateVertical: StoryFn = (args) => ( ); -export const VerticalThick: Story = { - render: TemplateVertical, +export const DotFilled: Story = { + render: () => { + return ( + <> + + + ); + }, +}; + +export const DotOutlined: Story = { + render: () => { + return ( + <> + + + ); + }, +}; + +const dotSizeToPxMap: Record = { + xs: '2px', + sm: '4px', + md: '8px', + lg: '15px', +}; + +const DottedSizesTemplate: StoryFn = (args) => { + const { array } = args; + + return ( +
+ {array.map((value, key) => ( + + + {value !== undefined ? dotSizeToPxMap[value] || value : '—'} + + + + + + + + + ))} +
+ ); +}; + +export const DottedSizes: StoryObj = { + render: DottedSizesTemplate, args: { - axis: 'horizontal', - thickness: 1, - isStretched: true, - topSpacing: 1, - bottomSpacing: 1, - md: { axis: 'vertical', thickness: 1 }, + property: 'dotSize', + array: ['extra-small', 'small', 'medium', 'large'], }, }; -export const VerticalDotted: Story = { +const InlineSeparatorTemplate: StoryFn = (args) => { + const { dotPosition, ...safeArgs } = args; + + return ( + <> + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + Lorem ipsum dolor sit, amet + + consectetur adipisicing elit. + + + ); +}; + +export const InlineSeparatorUsage: Story = { + render: InlineSeparatorTemplate, + args: { axis: 'vertical', display: 'inline' }, +}; + +export const VerticalDottedCardExample: Story = { render: TemplateVertical, args: { axis: 'horizontal', variant: 'dotted', color: 'accent', - topSpacing: 1, - bottomSpacing: 1, + spacing: 1, isStretched: true, + dotPosition: 1.25, md: { axis: 'vertical' }, }, }; -export const VerticalDottedSmall: Story = { +export const VerticalDottedSmallCardExample: Story = { render: TemplateVertical, args: { axis: 'horizontal', - topSpacing: 1, - bottomSpacing: 1, - variant: 'dotted-small', + spacing: 1, + variant: 'dotted', + dotSize: 'medium', color: 'accent', isStretched: true, + dotPosition: 1.25, md: { axis: 'vertical' }, }, }; - -export const HorizontalDottedSeparator: Story = { - render: () => ( - - - - - - - - - ), -}; - -export const DotOnly: Story = { - render: DotOnlyTemplate, - args: { spacing: 0.5 }, -}; - -const InlineSeparatorTemplate: StoryFn = (args) => ( - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - - Lorem ipsum dolor sit, amet - - consectetur adipisicing elit. - - -); - -export const InlineSeparatorUsedInText: Story = { - render: InlineSeparatorTemplate, - args: { axis: 'vertical', display: 'inline' }, -}; diff --git a/src/tedi/components/misc/separator/separator.tsx b/src/tedi/components/misc/separator/separator.tsx index bcf4f7682..cabe5bd3c 100644 --- a/src/tedi/components/misc/separator/separator.tsx +++ b/src/tedi/components/misc/separator/separator.tsx @@ -4,130 +4,162 @@ import { CSSProperties } from 'react'; import { BreakpointSupport, useBreakpointProps } from '../../../helpers'; import styles from './separator.module.scss'; -export type SeparatorSpacing = 0 | 0.25 | 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2 | 2.5 | 5; +export type SeparatorVariant = 'dotted' | 'dot-only'; +export type DotSize = 'large' | 'medium' | 'small' | 'extra-small'; +export type DotStyle = 'filled' | 'outlined'; +export type DotPosition = 'start' | 'center' | 'end' | number; + +/** + * Margin/padding-like spacing around the separator + * - number → uniform spacing on main axis + * - object → fine-grained control (top/bottom/left/right) + */ +export type SeparatorSpacing = + | number + | { + top?: number; + bottom?: number; + left?: number; + right?: number; + }; export interface SeparatorSharedProps { /** - * Additional class. + * Additional class names */ className?: string; /** - * Rendered HTML element. - * @default div + * HTML element to render — most common are 'hr', 'div', 'span' */ element?: 'hr' | 'div' | 'span'; /** - * Whether the separator should stretch to fill the full spacing inside cardContent. + * When true, the separator stretches to fill available space (100%) */ isStretched?: boolean; - /* - * Color of separator - * @default default + /** + * Semantic color token + * @default primary */ color?: 'primary' | 'secondary' | 'accent'; - /* - * Separator style variant. - */ - variant?: 'dotted' | 'dotted-small' | 'dot-only'; - /* - * Dot size. - * Only used when variant="dot-only" + /** + * Visual style — line with dots vs standalone centered dot(s) */ - dotSize?: 'large' | 'medium' | 'small' | 'extra-small'; - /* - * Thickness in pixels (ignored if variant is used). - * @default 1 + variant?: SeparatorVariant; + /** + * Line thickness in pixels (1 or 2) — affects outlined & solid lines */ thickness?: 1 | 2; /** - * Spacing applied based on the axis: - * - For horizontal axis, spacing is applied to top and bottom of the separator. - * - For vertical axis, spacing is applied to left and right of the separator. + * Spacing (margin) around the separator + * @example + * spacing={16} // 16px top & bottom (horizontal) or left & right (vertical) + * spacing={{ top: 24, bottom: 8 }} */ spacing?: SeparatorSpacing; } - export interface SeparatorVerticalProps extends SeparatorSharedProps { /** - * Height of separator. Use with vertical axis, when full-width separator is not needed. - * Height can be number in rem units. It's customizable to allow for more flexibility around X components. + * Must be set to 'vertical' + */ + axis: 'vertical'; + /** + * Height of the vertical separator in rem units */ height?: number; /** - * Axis of separator, vertical and horizontal separators support different props + * CSS display value — usually 'block' or 'inline-block' */ - axis: 'vertical'; - topSpacing?: undefined; - bottomSpacing?: undefined; - display?: 'block' | 'inline'; + display?: 'block' | 'inline' | 'inline-block'; } - export interface SeparatorHorizontalProps extends SeparatorSharedProps { /** - * Spacing on top of separator. Ignored when spacing is also used. Only for horizontal axis. + * Must be set to 'horizontal' or left undefined (defaults to horizontal) */ - topSpacing?: SeparatorSpacing; + axis?: 'horizontal'; /** - * Spacing on bottom of separator. Ignored when spacing is also used. Only for horizontal axis. - */ - bottomSpacing?: SeparatorSpacing; + Vertical height is not used in horizontal mode + */ + height?: undefined; /** - * Axis of separator, vertical and horizontal separators support different props + * Display is forced to 'block' in horizontal mode */ - axis?: 'horizontal'; - height?: undefined; display?: 'block'; } -export type SeparatorBreakpointProps = { +type DottedSeparatorProps = { + variant?: 'dotted'; + dotSize?: DotSize; + dotStyle?: DotStyle; /** - * Spacing values based on breakpoints. + * Position of the single dot + * @example + * 'center' | 'start' | 'end' | 2.5 // 2.5rem from start */ - spacing?: Omit; - /** - * Height values based on breakpoints (for vertical separators). - */ - height?: Omit; + dotPosition?: DotPosition; +}; + +type DotOnlySeparatorProps = { + variant: 'dot-only'; + dotSize: DotSize; + dotStyle?: DotStyle; + dotPosition?: never; +}; + +export type SeparatorBreakpointProps = { + spacing?: SeparatorSpacing; + height?: number; axis?: 'horizontal' | 'vertical'; }; export type SeparatorProps = BreakpointSupport< - | ( - | SeparatorHorizontalProps - | (SeparatorVerticalProps & { - variant?: 'dotted' | 'dotted-small'; - dotSize?: undefined; - }) - ) - | ( - | SeparatorHorizontalProps - | (SeparatorVerticalProps & { - variant: 'dot-only'; - dotSize: 'large' | 'medium' | 'small' | 'extra-small'; - }) - ) + | (SeparatorHorizontalProps & (DottedSeparatorProps | DotOnlySeparatorProps)) + | (SeparatorVerticalProps & (DottedSeparatorProps | DotOnlySeparatorProps)) > & SeparatorBreakpointProps; export const Separator = (props: SeparatorProps): JSX.Element => { const { getCurrentBreakpointProps } = useBreakpointProps(props.defaultServerBreakpoint); + const { className, element: Element = 'div', isStretched, spacing, - topSpacing, - bottomSpacing, axis = 'horizontal', - color = 'default', + color = 'primary', variant, thickness = 1, height, - dotSize, + dotSize = 'large', + dotStyle = 'filled', + dotPosition, display = 'block', ...rest } = getCurrentBreakpointProps(props); + const isNumericDotPosition = typeof dotPosition === 'number'; + const resolvedDotPosition = variant !== 'dot-only' && !isNumericDotPosition ? dotPosition : undefined; + + let top: number | undefined; + let bottom: number | undefined; + let left: number | undefined; + let right: number | undefined; + + if (typeof spacing === 'number') { + if (axis === 'horizontal') { + top = bottom = spacing; + left = right = 0; + } else { + left = right = spacing; + top = bottom = 0; + } + } else if (typeof spacing === 'object' && spacing !== null) { + top = spacing.top ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); + bottom = spacing.bottom ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); + left = spacing.left ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); + right = spacing.right ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); + } + const SeparatorBEM = cn( styles['tedi-separator'], className, @@ -135,19 +167,33 @@ export const Separator = (props: SeparatorProps): JSX.Element => { { [styles[`tedi-separator--${axis}`]]: axis }, { [styles[`tedi-separator--${variant}`]]: variant }, { [styles[`tedi-separator--${display}`]]: display }, - { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant && dotSize }, - { [styles[`tedi-separator--thickness-${thickness}`]]: thickness && !variant }, + { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant === 'dot-only' && dotSize }, + { [styles[`tedi-separator--dot-style-${dotStyle}`]]: variant && dotStyle }, + { [styles[`tedi-separator--dotted-${dotSize}`]]: variant === 'dotted' && dotSize }, + { [styles[`tedi-separator--dot-position-${resolvedDotPosition}`]]: resolvedDotPosition && variant !== 'dot-only' }, + { [styles['tedi-separator--dot-position-custom']]: isNumericDotPosition }, + { + [styles[`tedi-separator--thickness-${thickness}`]]: thickness || dotStyle === 'outlined' ? thickness : undefined, + }, { [styles['tedi-separator--is-stretched']]: isStretched }, - { [styles[`tedi-separator--spacing-${spacing}`.replace('.', '-')]]: spacing }, - { [styles[`tedi-separator--top-${topSpacing}`.replace('.', '-')]]: !spacing && topSpacing }, - { [styles[`tedi-separator--bottom-${bottomSpacing}`.replace('.', '-')]]: !spacing && bottomSpacing } + { [styles[`tedi-separator--top-${top}`.replace('.', '-')]]: top }, + { [styles[`tedi-separator--bottom-${bottom}`.replace('.', '-')]]: bottom }, + { [styles[`tedi-separator--left-${left}`.replace('.', '-')]]: left }, + { [styles[`tedi-separator--right-${right}`.replace('.', '-')]]: right } ); const getCssVars = () => { const cssvars: CSSProperties = {}; - if (height) cssvars['--vertical-separator-height'] = `${height}rem`; + if (thickness) { + cssvars['--separator-thickness'] = `${thickness}px`; + } + + if (variant === 'dotted' && isNumericDotPosition) { + cssvars['--separator-dot-position'] = `${dotPosition}rem`; + } + return cssvars; }; From abd15927725bedba73f5abde8988b6545d446645 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 25 Feb 2026 13:36:24 +0000 Subject: [PATCH 41/46] chore(release): 16.0.0-rc.18 # [16.0.0-rc.18](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.17...react-16.0.0-rc.18) (2026-02-25) ### Features * **separator:** redesign with new spacing API, dotPosition and dotStyle support [#30](https://github.com/TEDI-Design-System/react/issues/30) ([#525](https://github.com/TEDI-Design-System/react/issues/525)) ([93e9e11](https://github.com/TEDI-Design-System/react/commit/93e9e118aab73d40347029089c5370851a7b551c)) ### BREAKING CHANGES * **separator:** legacy spacing props removed, dotSize enum changed, variant values simplified --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 772692d48..e102a9e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [16.0.0-rc.18](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.17...react-16.0.0-rc.18) (2026-02-25) + + +### Features + +* **separator:** redesign with new spacing API, dotPosition and dotStyle support [#30](https://github.com/TEDI-Design-System/react/issues/30) ([#525](https://github.com/TEDI-Design-System/react/issues/525)) ([93e9e11](https://github.com/TEDI-Design-System/react/commit/93e9e118aab73d40347029089c5370851a7b551c)) + + +### BREAKING CHANGES + +* **separator:** legacy spacing props removed, dotSize enum changed, variant values simplified + # [16.0.0-rc.17](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.16...react-16.0.0-rc.17) (2026-02-25) From bd8bd1640c40be40199d881efead665ce45bd0a3 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:38:07 +0200 Subject: [PATCH 42/46] fix(text-field): fix small textfield right area padding, fix small textfield font size #527 (#528) --- .../form/textfield/textfield.module.scss | 131 +++++++++--------- 1 file changed, 63 insertions(+), 68 deletions(-) diff --git a/src/tedi/components/form/textfield/textfield.module.scss b/src/tedi/components/form/textfield/textfield.module.scss index 850f10a8f..fa91f068c 100644 --- a/src/tedi/components/form/textfield/textfield.module.scss +++ b/src/tedi/components/form/textfield/textfield.module.scss @@ -1,7 +1,32 @@ @use '@tedi-design-system/core/bootstrap-utility/breakpoints'; @use '@tedi-design-system/core/mixins'; +@use 'sass:map'; $input-height-large: 3.5rem; +$input-padding-right-map: ( + default: ( + normal: 2rem, + both: 4rem, + ), + small: ( + normal: 2rem, + both: 4rem, + ), + large: ( + normal: 3.5rem, + both: 6rem, + ), +); + +@function get-padding-right($size, $clearable, $icon) { + @if $clearable and $icon { + @return map.get(map.get($input-padding-right-map, $size), both); + } @else if $clearable or $icon { + @return map.get(map.get($input-padding-right-map, $size), normal); + } @else { + @return null; + } +} @mixin textfield-focus($border-color: null) { @if $border-color { @@ -69,29 +94,45 @@ $input-height-large: 3.5rem; @include textfield-focus(var(--form-general-feedback-success-border)); } - .tedi-textfield--clearable &, - .tedi-textfield--with-icon & { - padding-right: 2rem; - } - - .tedi-textfield--clearable.tedi-textfield--with-icon & { - padding-right: 4rem; - } - &::placeholder { color: var(--form-input-text-placeholder); opacity: 1; } } -// hide arrows on number inputs -.tedi-textfield__input--hidden-arrows[type='number'] { - appearance: textfield; // for Firefox +@each $size in default, small, large { + $selector: if($size == default, '.tedi-textfield', '.tedi-textfield--#{$size}'); + #{$selector} { + $padding: get-padding-right($size, true, false); - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - margin: 0; - appearance: none; + &.tedi-textfield--clearable .tedi-textfield__input { + padding-right: get-padding-right($size, true, false); + } + + &.tedi-textfield--with-icon .tedi-textfield__input { + padding-right: get-padding-right($size, false, true); + } + + &.tedi-textfield--clearable.tedi-textfield--with-icon .tedi-textfield__input { + padding-right: get-padding-right($size, true, true); + } + + @if $size == small { + .tedi-textfield__input { + height: var(--form-field-height-sm); + padding: var(--form-field-padding-y-sm) var(--form-field-padding-x-md-default); + font-size: var(--body-regular-size); + } + } @else if $size == large { + .tedi-textfield__input { + height: $input-height-large; + padding: var(--form-field-padding-y-lg) var(--form-field-padding-x-lg); + + @include breakpoints.media-breakpoint-down(md) { + height: var(--form-field-height); + } + } + } } } @@ -100,26 +141,24 @@ $input-height-large: 3.5rem; top: 0; right: 0; display: flex; + gap: var(--layout-grid-gutters-04); align-items: center; height: 100%; - padding: 0.75rem 0.5rem; + padding: var(--form-field-padding-y-md-default) var(--form-field-padding-x-md-default); .tedi-textfield--small & { - padding: 0.25rem 0.5rem; + padding: var(--form-field-padding-y-sm) var(--form-field-padding-x-md-default); } .tedi-textfield--large & { - padding: 1rem; + padding: var(--form-field-padding-y-lg) var(--form-field-padding-x-lg); } } -.tedi-textfield__separator { - margin-right: 0.5rem; - margin-left: 0.25rem; -} - .tedi-textfield__icon-wrapper { display: flex; + justify-content: center; + width: var(--icon-04); color: var(--form-input-text-filled); transition: color 0.2s ease; @@ -150,50 +189,6 @@ div.tedi-textfield__icon-wrapper { vertical-align: text-top; } -/* Small */ -.tedi-textfield--small { - .tedi-textfield__input { - height: var(--form-field-height-sm); - padding: var(--form-field-padding-y-sm) var(--form-field-padding-x-md-default); - font-size: var(--body-small-regular-size); - } - - &.tedi-textfield--clearable &, - &.tedi-textfield--with-icon & { - padding-right: 2rem; - } - - &.tedi-textfield--clearable.tedi-textfield--with-icon & { - padding-right: 4rem; - } -} - -/* Large */ -.tedi-textfield--large { - .tedi-textfield__input { - height: $input-height-large; - padding: 1rem; - - @include breakpoints.media-breakpoint-down(md) { - height: var(--form-field-height); - } - } - - .tedi-textfield__separator { - margin-right: 0.5rem; - margin-left: 0.5rem; - } - - &.tedi-textfield--clearable .tedi-textfield__input, - &.tedi-textfield--with-icon .tedi-textfield__input { - padding-right: 3.5rem; - } - - &.tedi-textfield--clearable.tedi-textfield--with-icon .tedi-textfield__input { - padding-right: 6rem; - } -} - .tedi-textfield__feedback-wrapper { display: flex; } From 8f1f7f634f002a13c10c2cfeffbeabf27759d7ed Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 25 Feb 2026 13:42:23 +0000 Subject: [PATCH 43/46] chore(release): 16.0.0-rc.19 # [16.0.0-rc.19](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.18...react-16.0.0-rc.19) (2026-02-25) ### Bug Fixes * **text-field:** fix small textfield right area padding, fix small textfield font size [#527](https://github.com/TEDI-Design-System/react/issues/527) ([#528](https://github.com/TEDI-Design-System/react/issues/528)) ([bd8bd16](https://github.com/TEDI-Design-System/react/commit/bd8bd1640c40be40199d881efead665ce45bd0a3)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e102a9e69..bc9a964cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.19](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.18...react-16.0.0-rc.19) (2026-02-25) + + +### Bug Fixes + +* **text-field:** fix small textfield right area padding, fix small textfield font size [#527](https://github.com/TEDI-Design-System/react/issues/527) ([#528](https://github.com/TEDI-Design-System/react/issues/528)) ([bd8bd16](https://github.com/TEDI-Design-System/react/commit/bd8bd1640c40be40199d881efead665ce45bd0a3)) + # [16.0.0-rc.18](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.17...react-16.0.0-rc.18) (2026-02-25) From 9d23abfb289ca1fd5c259827102744be74c39ac6 Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:08:21 +0200 Subject: [PATCH 44/46] chore: move grid stories under Layout folder #526 (#529) * chore: move grid stories under Layout folder #526 * chore: move VerticalSpacing story to Layout as well #526 * chore: fix bootstrap typo #526 --- .../docs/scale-layout/development.mdx | 12 -- .../components/layout/grid/col.stories.tsx | 59 +++++++++ .../components/layout/grid/grid.stories.tsx | 120 ------------------ .../components/layout/grid/row.stories.tsx | 59 +++++++++ .../vertical-spacing.stories.tsx | 2 +- .../docs/scale-layout/breaking-points.tsx | 4 +- .../docs/scale-layout/grid.mdx | 2 +- .../docs/scale-layout/grid.tsx | 8 +- .../docs/scale-layout/spacing.mdx | 2 +- .../docs/scale-layout/spacing.tsx | 10 +- 10 files changed, 132 insertions(+), 146 deletions(-) delete mode 100644 src/community/docs/scale-layout/development.mdx create mode 100644 src/tedi/components/layout/grid/col.stories.tsx delete mode 100644 src/tedi/components/layout/grid/grid.stories.tsx create mode 100644 src/tedi/components/layout/grid/row.stories.tsx rename src/{community => tedi}/docs/scale-layout/breaking-points.tsx (96%) rename src/{community => tedi}/docs/scale-layout/grid.mdx (59%) rename src/{community => tedi}/docs/scale-layout/grid.tsx (92%) rename src/{community => tedi}/docs/scale-layout/spacing.mdx (60%) rename src/{community => tedi}/docs/scale-layout/spacing.tsx (88%) diff --git a/src/community/docs/scale-layout/development.mdx b/src/community/docs/scale-layout/development.mdx deleted file mode 100644 index a413dfb5c..000000000 --- a/src/community/docs/scale-layout/development.mdx +++ /dev/null @@ -1,12 +0,0 @@ -import { Meta, Title, Subtitle, Unstyled } from '@storybook/blocks'; - - - -# Grid in development - -Every layout in design is makeable with `` and `` components.
-All examples of Grid usage are under [Grid docs](/docs/components-grid--docs) - -To create vertical spacing between elements use `` [component](/docs/components-vertical-spacing--docs). - -{/* TODO: Need more information how to use Row, Col, VerticalSpacing and Card correctly */} diff --git a/src/tedi/components/layout/grid/col.stories.tsx b/src/tedi/components/layout/grid/col.stories.tsx new file mode 100644 index 000000000..8f7925529 --- /dev/null +++ b/src/tedi/components/layout/grid/col.stories.tsx @@ -0,0 +1,59 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react'; + +import { Heading } from '../../base/typography/heading/heading'; +import { VerticalSpacing } from '../vertical-spacing'; +import { Col } from './col'; +import { Row } from './row'; + +/** + * Zeroheight ↗
+ * Bootstrap docs ↗
+ * Row and Col components are inspired by Bootstrap V5 Grid System.
You can use different Bootstrap grid classes + * through component props. + */ + +const meta: Meta = { + title: 'Tedi-Ready/Layout/Grid/Col', + component: Col, + subcomponents: { Row } as never, + parameters: { + status: { type: 'devComponent' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: StoryFn = () => ( + + + Label/Value pairs + + +
+ + + Label + + Value + + + + Label + + Value + +
+ +
+
+ + + Lists + + Item 1 + Item 2 + + +
+); diff --git a/src/tedi/components/layout/grid/grid.stories.tsx b/src/tedi/components/layout/grid/grid.stories.tsx deleted file mode 100644 index 78ec8ed9d..000000000 --- a/src/tedi/components/layout/grid/grid.stories.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Meta, StoryFn, StoryObj } from '@storybook/react'; - -import { Heading } from '../../base/typography/heading/heading'; -import { VerticalSpacing } from '../vertical-spacing'; -import { Col } from './col'; -import { Row, RowProps } from './row'; - -/** - * Zeroheight ↗
- * Boostrap docs ↗
- * Row and Col components are inspired by Bootstrap V5 Grid System.
You can use different Bootstrap grid classes - * through component props. - */ -const meta: Meta = { - title: 'Tedi-Ready/Components/Helpers/Grid', - component: Row, - subcomponents: { Col: Col } as never, - parameters: { - status: { - type: 'devComponent', - }, - }, -}; - -export default meta; -type Story = StoryObj; - -const Template: StoryFn = (args) => ( -
- - Col-1 - Col-2 - Col-3 - - - - Col-1 - - - Col-2 - - - Col-3 - - - - - Col-1 - - - Col-2 - - - Col-3 - - - - Col-1 - Col-2 - Col-3 - -
-); - -export const Default: Story = { - render: Template, -}; - -export const CustomTags: StoryFn = () => ( - - - Label/Value pairs - - - {/* Grouped rows */} -
- - - Label - - Value - - - - Label - - Value - -
- - - {/* Non grouped rows */} - - - - Label - - - Value - - - Label - - - Value - - - -
-
- - - Lists - - Item 1 - Item 2 - - -
-); diff --git a/src/tedi/components/layout/grid/row.stories.tsx b/src/tedi/components/layout/grid/row.stories.tsx new file mode 100644 index 000000000..8fb61400d --- /dev/null +++ b/src/tedi/components/layout/grid/row.stories.tsx @@ -0,0 +1,59 @@ +import { Meta, StoryFn, StoryObj } from '@storybook/react'; + +import { Col } from './col'; +import { Row, RowProps } from './row'; + +/** + * Zeroheight ↗
+ * Bootstrap docs ↗
+ * Row and Col components are inspired by Bootstrap V5 Grid System.
You can use different Bootstrap grid classes + * through component props. + */ + +const meta: Meta = { + title: 'Tedi-Ready/Layout/Grid/Row', + component: Row, + subcomponents: { Col } as never, + parameters: { + status: { type: 'devComponent' }, + }, +}; + +export default meta; +type Story = StoryObj; + +const Template: StoryFn = (args) => ( +
+ + Col-1 + Col-2 + Col-3 + + + + + Col-1 + + + Col-2 + + + Col-3 + + + + + + Col-1 + + + Col-2 + + + Col-3 + + +
+); + +export const Default: Story = { render: Template }; diff --git a/src/tedi/components/layout/vertical-spacing/vertical-spacing.stories.tsx b/src/tedi/components/layout/vertical-spacing/vertical-spacing.stories.tsx index 4af447580..a3fcaffe9 100644 --- a/src/tedi/components/layout/vertical-spacing/vertical-spacing.stories.tsx +++ b/src/tedi/components/layout/vertical-spacing/vertical-spacing.stories.tsx @@ -11,7 +11,7 @@ import { VerticalSpacing, VerticalSpacingProps } from './vertical-spacing'; const meta: Meta = { component: VerticalSpacing, - title: 'Tedi-Ready/Components/Helpers/VerticalSpacing', + title: 'Tedi-Ready/Layout/VerticalSpacing', subcomponents: { 'VerticalSpacing.Item': VerticalSpacing.Item, } as never, diff --git a/src/community/docs/scale-layout/breaking-points.tsx b/src/tedi/docs/scale-layout/breaking-points.tsx similarity index 96% rename from src/community/docs/scale-layout/breaking-points.tsx rename to src/tedi/docs/scale-layout/breaking-points.tsx index 174567782..ccb5c4244 100644 --- a/src/community/docs/scale-layout/breaking-points.tsx +++ b/src/tedi/docs/scale-layout/breaking-points.tsx @@ -1,7 +1,7 @@ import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; -import { Text } from '../../../tedi/components/base/typography/text/text'; -import { Breakpoint, CustomizeTableCell, getBackgroundColorClass, Table } from '../../index'; +import { Breakpoint, CustomizeTableCell, getBackgroundColorClass, Table } from '../../../community/index'; +import { Text } from '../../components/base/typography/text/text'; interface BreakpointRow { type: 'mobile' | 'tablet' | 'desktop'; diff --git a/src/community/docs/scale-layout/grid.mdx b/src/tedi/docs/scale-layout/grid.mdx similarity index 59% rename from src/community/docs/scale-layout/grid.mdx rename to src/tedi/docs/scale-layout/grid.mdx index 907fc622f..6d4bb22fd 100644 --- a/src/community/docs/scale-layout/grid.mdx +++ b/src/tedi/docs/scale-layout/grid.mdx @@ -1,7 +1,7 @@ import { Meta, Title, Subtitle, Unstyled } from '@storybook/blocks'; import Grid from './grid.tsx'; - + diff --git a/src/community/docs/scale-layout/grid.tsx b/src/tedi/docs/scale-layout/grid.tsx similarity index 92% rename from src/community/docs/scale-layout/grid.tsx rename to src/tedi/docs/scale-layout/grid.tsx index e89db1d09..04998c322 100644 --- a/src/community/docs/scale-layout/grid.tsx +++ b/src/tedi/docs/scale-layout/grid.tsx @@ -1,9 +1,9 @@ import { Title } from '@storybook/blocks'; -import { Heading } from '../../../tedi/components/base/typography/heading/heading'; -import { Col, Row } from '../../../tedi/components/layout/grid'; -import { VerticalSpacing } from '../../../tedi/components/layout/vertical-spacing'; -import { Separator } from '../../../tedi/components/misc/separator/separator'; +import { Heading } from '../../components/base/typography/heading/heading'; +import { Col, Row } from '../../components/layout/grid'; +import { VerticalSpacing } from '../../components/layout/vertical-spacing'; +import { Separator } from '../../components/misc/separator/separator'; import BreakingpointsTable from './breaking-points'; const Grid = () => { diff --git a/src/community/docs/scale-layout/spacing.mdx b/src/tedi/docs/scale-layout/spacing.mdx similarity index 60% rename from src/community/docs/scale-layout/spacing.mdx rename to src/tedi/docs/scale-layout/spacing.mdx index 547603667..4dd24520b 100644 --- a/src/community/docs/scale-layout/spacing.mdx +++ b/src/tedi/docs/scale-layout/spacing.mdx @@ -2,7 +2,7 @@ import { Meta, Unstyled } from '@storybook/blocks'; import Spacing from './spacing.tsx'; - + diff --git a/src/community/docs/scale-layout/spacing.tsx b/src/tedi/docs/scale-layout/spacing.tsx similarity index 88% rename from src/community/docs/scale-layout/spacing.tsx rename to src/tedi/docs/scale-layout/spacing.tsx index f22458635..03fe96afa 100644 --- a/src/community/docs/scale-layout/spacing.tsx +++ b/src/tedi/docs/scale-layout/spacing.tsx @@ -1,11 +1,11 @@ import { Title } from '@storybook/blocks'; import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; -import { Heading } from '../../../tedi/components/base/typography/heading/heading'; -import { Col, Row } from '../../../tedi/components/layout/grid'; -import { VerticalSpacing } from '../../../tedi/components/layout/vertical-spacing'; -import { Separator } from '../../../tedi/components/misc/separator/separator'; -import { Table } from '../../index'; +import { Table } from '../../../community/index'; +import { Heading } from '../../components/base/typography/heading/heading'; +import { Col, Row } from '../../components/layout/grid'; +import { VerticalSpacing } from '../../components/layout/vertical-spacing'; +import { Separator } from '../../components/misc/separator/separator'; const Spacing = () => { return ( From 130edcd158944b90c62788e76fc97e015f1f228c Mon Sep 17 00:00:00 2001 From: Airike Jaska <95303654+airikej@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:27:54 +0200 Subject: [PATCH 45/46] fix(separator): fix custom size application #535 (#536) * fix(separator): fix custom size application #535 * chore: fix grid stories typo #535 * fix(separator): removed unnecessary zeros #535 --- .../components/layout/grid/col.stories.tsx | 2 +- .../components/layout/grid/row.stories.tsx | 2 +- .../misc/separator/separator.module.scss | 40 +--------- .../misc/separator/separator.spec.tsx | 21 +++-- .../components/misc/separator/separator.tsx | 77 ++++++++++--------- 5 files changed, 57 insertions(+), 85 deletions(-) diff --git a/src/tedi/components/layout/grid/col.stories.tsx b/src/tedi/components/layout/grid/col.stories.tsx index 8f7925529..b2ea48bf9 100644 --- a/src/tedi/components/layout/grid/col.stories.tsx +++ b/src/tedi/components/layout/grid/col.stories.tsx @@ -7,7 +7,7 @@ import { Row } from './row'; /** * Zeroheight ↗
- * Bootstrap docs ↗
+ * Bootstrap docs ↗
* Row and Col components are inspired by Bootstrap V5 Grid System.
You can use different Bootstrap grid classes * through component props. */ diff --git a/src/tedi/components/layout/grid/row.stories.tsx b/src/tedi/components/layout/grid/row.stories.tsx index 8fb61400d..1904bb382 100644 --- a/src/tedi/components/layout/grid/row.stories.tsx +++ b/src/tedi/components/layout/grid/row.stories.tsx @@ -5,7 +5,7 @@ import { Row, RowProps } from './row'; /** * Zeroheight ↗
- * Bootstrap docs ↗
+ * Bootstrap docs ↗
* Row and Col components are inspired by Bootstrap V5 Grid System.
You can use different Bootstrap grid classes * through component props. */ diff --git a/src/tedi/components/misc/separator/separator.module.scss b/src/tedi/components/misc/separator/separator.module.scss index 6d648458d..85741e1e7 100644 --- a/src/tedi/components/misc/separator/separator.module.scss +++ b/src/tedi/components/misc/separator/separator.module.scss @@ -1,18 +1,5 @@ @use '@tedi-design-system/core/mixins'; -$sizes: ( - '0': 0, - '0-25': 0.25rem, - '0-5': 0.5rem, - '0-75': 0.75rem, - '1': 1rem, - '1-25': 1.25rem, - '1-5': 1.5rem, - '1-75': 1.75rem, - '2': 2rem, - '2-5': 2.5rem, - '5': 5rem, -); $thicknesses: ( '1': 1px, '2': 2px, @@ -27,7 +14,8 @@ $dot-colors: ( --vertical-separator-height: 100%; position: relative; - margin: 0; + margin: var(--separator-margin-top) var(--separator-margin-right) var(--separator-margin-bottom) + var(--separator-margin-left); border: 0; border-top: 1px solid var(--general-border-primary); @@ -172,30 +160,6 @@ $dot-colors: ( bottom: auto; } -@each $size, $value in $sizes { - .tedi-separator--spacing-#{$size}:not(.tedi-separator--dot-only) { - margin: #{$value} 0; - } - - .tedi-separator--dot-only.tedi-separator--spacing-#{$size} { - &.tedi-separator--horizontal { - margin-right: $value; - margin-left: $value; - } - - &.tedi-separator--vertical { - margin-top: $value; - margin-bottom: $value; - } - } - - @each $side in (top, bottom, left, right) { - .tedi-separator--#{$side}-#{$size} { - margin-#{$side}: $value; - } - } -} - @each $thickness, $value in $thicknesses { .tedi-separator--thickness-#{$thickness} { border-top-width: $value; diff --git a/src/tedi/components/misc/separator/separator.spec.tsx b/src/tedi/components/misc/separator/separator.spec.tsx index 694961d9a..0f465e37e 100644 --- a/src/tedi/components/misc/separator/separator.spec.tsx +++ b/src/tedi/components/misc/separator/separator.spec.tsx @@ -57,24 +57,31 @@ describe('Separator Component', () => { expect(el).toHaveStyle('--vertical-separator-height: 5.5rem'); }); - it('applies spacing classes — number value', () => { + it('applies spacing — number value', () => { const { getByTestId } = renderComponent({ spacing: 1.5 }); const el = getByTestId('separator'); - expect(el).toHaveClass(styles['tedi-separator--top-1-5']); - expect(el).toHaveClass(styles['tedi-separator--bottom-1-5']); + expect(el).toHaveStyle({ + '--separator-margin-top': '1.5rem', + '--separator-margin-bottom': '1.5rem', + '--separator-margin-left': '0rem', + '--separator-margin-right': '0rem', + }); }); - it('applies spacing classes — object value', () => { + it('applies spacing — object value', () => { const { getByTestId } = renderComponent({ spacing: { top: 2, bottom: 0.5, left: 1 }, axis: 'horizontal', }); const el = getByTestId('separator'); - expect(el).toHaveClass(styles['tedi-separator--top-2']); - expect(el).toHaveClass(styles['tedi-separator--bottom-0-5']); - expect(el).toHaveClass(styles['tedi-separator--left-1']); + expect(el).toHaveStyle({ + '--separator-margin-top': '2rem', + '--separator-margin-bottom': '0.5rem', + '--separator-margin-left': '1rem', + '--separator-margin-right': '0rem', + }); }); it('applies isStretched class', () => { diff --git a/src/tedi/components/misc/separator/separator.tsx b/src/tedi/components/misc/separator/separator.tsx index cabe5bd3c..d343f08b3 100644 --- a/src/tedi/components/misc/separator/separator.tsx +++ b/src/tedi/components/misc/separator/separator.tsx @@ -140,64 +140,65 @@ export const Separator = (props: SeparatorProps): JSX.Element => { const isNumericDotPosition = typeof dotPosition === 'number'; const resolvedDotPosition = variant !== 'dot-only' && !isNumericDotPosition ? dotPosition : undefined; - let top: number | undefined; - let bottom: number | undefined; - let left: number | undefined; - let right: number | undefined; + let top = 0; + let bottom = 0; + let left = 0; + let right = 0; if (typeof spacing === 'number') { if (axis === 'horizontal') { top = bottom = spacing; - left = right = 0; } else { left = right = spacing; - top = bottom = 0; } - } else if (typeof spacing === 'object' && spacing !== null) { - top = spacing.top ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); - bottom = spacing.bottom ?? (axis === 'horizontal' ? spacing.top ?? spacing.bottom ?? 0 : 0); - left = spacing.left ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); - right = spacing.right ?? (axis === 'vertical' ? spacing.left ?? spacing.right ?? 0 : 0); + } + + if (typeof spacing === 'object' && spacing !== null) { + top = spacing.top ?? top; + bottom = spacing.bottom ?? bottom; + left = spacing.left ?? left; + right = spacing.right ?? right; } const SeparatorBEM = cn( styles['tedi-separator'], className, - { [styles[`tedi-separator--${color}`]]: color }, - { [styles[`tedi-separator--${axis}`]]: axis }, - { [styles[`tedi-separator--${variant}`]]: variant }, - { [styles[`tedi-separator--${display}`]]: display }, - { [styles[`tedi-separator--${variant}-${dotSize}`]]: variant === 'dot-only' && dotSize }, - { [styles[`tedi-separator--dot-style-${dotStyle}`]]: variant && dotStyle }, - { [styles[`tedi-separator--dotted-${dotSize}`]]: variant === 'dotted' && dotSize }, - { [styles[`tedi-separator--dot-position-${resolvedDotPosition}`]]: resolvedDotPosition && variant !== 'dot-only' }, - { [styles['tedi-separator--dot-position-custom']]: isNumericDotPosition }, + styles[`tedi-separator--${axis}`], + styles[`tedi-separator--${color}`], { + [styles[`tedi-separator--${variant}`]]: variant, + [styles[`tedi-separator--${display}`]]: display, + [styles[`tedi-separator--dotted-${dotSize}`]]: variant === 'dotted', + [styles[`tedi-separator--dot-only-${dotSize}`]]: variant === 'dot-only', + [styles[`tedi-separator--dot-style-${dotStyle}`]]: variant, + [styles[`tedi-separator--dot-position-${resolvedDotPosition}`]]: + typeof resolvedDotPosition === 'string' && variant !== 'dot-only', + [styles['tedi-separator--dot-position-custom']]: isNumericDotPosition, + [styles['tedi-separator--is-stretched']]: isStretched, [styles[`tedi-separator--thickness-${thickness}`]]: thickness || dotStyle === 'outlined' ? thickness : undefined, - }, - { [styles['tedi-separator--is-stretched']]: isStretched }, - { [styles[`tedi-separator--top-${top}`.replace('.', '-')]]: top }, - { [styles[`tedi-separator--bottom-${bottom}`.replace('.', '-')]]: bottom }, - { [styles[`tedi-separator--left-${left}`.replace('.', '-')]]: left }, - { [styles[`tedi-separator--right-${right}`.replace('.', '-')]]: right } + } ); - const getCssVars = () => { - const cssvars: CSSProperties = {}; - if (height) cssvars['--vertical-separator-height'] = `${height}rem`; + const cssVars: CSSProperties = { + '--separator-margin-top': `${top}rem`, + '--separator-margin-bottom': `${bottom}rem`, + '--separator-margin-left': `${left}rem`, + '--separator-margin-right': `${right}rem`, + } as CSSProperties; - if (thickness) { - cssvars['--separator-thickness'] = `${thickness}px`; - } + if (height) { + cssVars['--vertical-separator-height'] = `${height}rem`; + } - if (variant === 'dotted' && isNumericDotPosition) { - cssvars['--separator-dot-position'] = `${dotPosition}rem`; - } + if (thickness) { + cssVars['--separator-thickness'] = `${thickness}px`; + } - return cssvars; - }; + if (variant === 'dotted' && isNumericDotPosition) { + cssVars['--separator-dot-position'] = `${dotPosition}rem`; + } - return ; + return ; }; export default Separator; From 8fc49d6aa9c27140254072eb43d618e2a6a3438c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 26 Feb 2026 10:32:15 +0000 Subject: [PATCH 46/46] chore(release): 16.0.0-rc.20 # [16.0.0-rc.20](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.19...react-16.0.0-rc.20) (2026-02-26) ### Bug Fixes * **separator:** fix custom size application [#535](https://github.com/TEDI-Design-System/react/issues/535) ([#536](https://github.com/TEDI-Design-System/react/issues/536)) ([130edcd](https://github.com/TEDI-Design-System/react/commit/130edcd158944b90c62788e76fc97e015f1f228c)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9a964cb..3d8e36d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [16.0.0-rc.20](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.19...react-16.0.0-rc.20) (2026-02-26) + + +### Bug Fixes + +* **separator:** fix custom size application [#535](https://github.com/TEDI-Design-System/react/issues/535) ([#536](https://github.com/TEDI-Design-System/react/issues/536)) ([130edcd](https://github.com/TEDI-Design-System/react/commit/130edcd158944b90c62788e76fc97e015f1f228c)) + # [16.0.0-rc.19](https://github.com/TEDI-Design-System/react/compare/react-16.0.0-rc.18...react-16.0.0-rc.19) (2026-02-25)