From 749f62593214786e21534275cbc71c47314e9bcf Mon Sep 17 00:00:00 2001 From: Nandor_Czegledi Date: Tue, 28 Jul 2026 09:57:36 +0200 Subject: [PATCH] feat(many): add margin prop to v2 FormFieldGroup, CheckboxGroup, RadioInputGroup, Checkbox, RadioInput, RangeInput, Text, and ToggleButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional `margin` prop (Spacing) to the v2 versions of the listed components, matching the established v2 layout-spacing pattern. Leaf components (Text, Checkbox, RadioInput) resolve it in their styles via `calcSpacingFromShorthand` against `sharedTokens.spacing` + `sharedTokens.legacy.spacing`. The rest forward it to an already margin-capable child: ToggleButton to IconButton, RangeInput to FormField, and the groups to FormFieldGroup/FormFieldLayout. Accepts current dot-path spacing tokens (e.g. `general.spaceMd`), CSS-like 1-4 value shorthand, and raw CSS values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/ToggleButton.test.tsx | 16 ++++++++++++++ .../ui-buttons/src/ToggleButton/v2/index.tsx | 2 ++ .../ui-buttons/src/ToggleButton/v2/props.ts | 9 ++++++++ .../src/Checkbox/__tests__/Checkbox.test.tsx | 16 ++++++++++++++ packages/ui-checkbox/src/Checkbox/v2/props.ts | 13 ++++++++++- .../ui-checkbox/src/Checkbox/v2/styles.ts | 13 ++++++++--- .../__tests__/CheckboxGroup.test.tsx | 12 ++++++++++ .../src/CheckboxGroup/v2/README.md | 1 + .../ui-checkbox/src/CheckboxGroup/v2/props.ts | 10 ++++++++- .../__tests__/FormFieldGroup.test.tsx | 16 ++++++++++++++ .../src/FormFieldGroup/v2/README.md | 1 + .../src/FormFieldGroup/v2/props.ts | 13 ++++++++++- .../RadioInput/__tests__/RadioInput.test.tsx | 16 ++++++++++++++ .../src/RadioInput/v2/index.tsx | 4 +++- .../ui-radio-input/src/RadioInput/v2/props.ts | 13 ++++++++++- .../src/RadioInput/v2/styles.ts | 22 +++++++++++++++++-- .../__tests__/RadioInputGroup.test.tsx | 19 ++++++++++++++++ .../src/RadioInputGroup/v2/README.md | 2 +- .../src/RadioInputGroup/v2/props.ts | 11 +++++++++- .../RangeInput/__tests__/RangeInput.test.tsx | 16 ++++++++++++++ .../src/RangeInput/v2/README.md | 1 + .../ui-range-input/src/RangeInput/v2/props.ts | 14 +++++++++++- .../ui-text/src/Text/__tests__/Text.test.tsx | 16 ++++++++++++++ packages/ui-text/src/Text/v2/README.md | 15 +++++++++++++ packages/ui-text/src/Text/v2/props.ts | 15 +++++++++++-- packages/ui-text/src/Text/v2/styles.ts | 16 +++++++++++--- 26 files changed, 284 insertions(+), 18 deletions(-) diff --git a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx index 84e483ce5b..7a24624c8a 100644 --- a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx +++ b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx @@ -243,4 +243,20 @@ describe('', () => { await expect.element(tooltip).toBeVisible() await expect.element(tooltip).toHaveTextContent('Tooltip content') }) + + it('forwards the margin prop to the button', async () => { + const { container } = await render( + + ) + const button = container.querySelector('button') + + // general.spaceMd (0.75rem) resolves to 12px + expect(getComputedStyle(button!).marginLeft).toBe('12px') + }) }) diff --git a/packages/ui-buttons/src/ToggleButton/v2/index.tsx b/packages/ui-buttons/src/ToggleButton/v2/index.tsx index 22bd7cde89..6d4f3d6eb6 100644 --- a/packages/ui-buttons/src/ToggleButton/v2/index.tsx +++ b/packages/ui-buttons/src/ToggleButton/v2/index.tsx @@ -91,6 +91,7 @@ class ToggleButton extends Component { status, placement, onClick, + margin, ...props } = this.props @@ -124,6 +125,7 @@ class ToggleButton extends Component { aria-pressed={status === 'pressed'} data-cid="ToggleButton" renderIcon={renderIcon} + margin={margin} /> ) diff --git a/packages/ui-buttons/src/ToggleButton/v2/props.ts b/packages/ui-buttons/src/ToggleButton/v2/props.ts index 189fb6577b..10b575d6d9 100644 --- a/packages/ui-buttons/src/ToggleButton/v2/props.ts +++ b/packages/ui-buttons/src/ToggleButton/v2/props.ts @@ -35,6 +35,7 @@ import type { } from '@instructure/ui-position' import type { ViewProps } from '@instructure/ui-view/latest' import { Renderable } from '@instructure/shared-types' +import type { Spacing } from '@instructure/emotion' type ToggleButtonOwnProps = { /** @@ -110,6 +111,13 @@ type ToggleButtonOwnProps = { * or a function returning an element. */ constrain?: PositionConstraint + + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing } type PropKeys = keyof ToggleButtonOwnProps @@ -129,6 +137,7 @@ const allowedProps: AllowedPropKeys = [ 'elementRef', 'interaction', 'isShowingTooltip', + 'margin', 'mountNode', 'onClick', 'placement', diff --git a/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx b/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx index e3c3fd7e33..326433b10f 100644 --- a/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx +++ b/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx @@ -352,4 +352,20 @@ describe('', () => { expect(input).not.toHaveAttribute('aria-labelledby') }) }) + + describe('margin prop', () => { + it('resolves spacing tokens and custom CSS values', async () => { + const { container } = await render( +
+ + +
+ ) + const boxes = container.querySelectorAll("[class$='-checkbox']") + + // general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(boxes[0]!).marginLeft).toBe('12px') + expect(getComputedStyle(boxes[1]!).marginLeft).toBe('30px') + }) + }) }) diff --git a/packages/ui-checkbox/src/Checkbox/v2/props.ts b/packages/ui-checkbox/src/Checkbox/v2/props.ts index cc66568be5..ba022306ac 100644 --- a/packages/ui-checkbox/src/Checkbox/v2/props.ts +++ b/packages/ui-checkbox/src/Checkbox/v2/props.ts @@ -24,7 +24,11 @@ import type { FormMessage } from '@instructure/ui-form-field/latest' import type { OtherHTMLAttributes } from '@instructure/shared-types' -import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { + WithStyleProps, + ComponentStyle, + Spacing +} from '@instructure/emotion' import type { NewComponentTypes } from '@instructure/ui-themes' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' @@ -71,6 +75,12 @@ type CheckboxOwnProps = { inline?: boolean labelPlacement?: 'top' | 'start' | 'end' isRequired?: boolean + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing /** * A function that provides a reference to the actual underlying input element */ @@ -116,6 +126,7 @@ const allowedProps: AllowedPropKeys = [ 'inline', 'labelPlacement', 'isRequired', + 'margin', 'inputRef' ] diff --git a/packages/ui-checkbox/src/Checkbox/v2/styles.ts b/packages/ui-checkbox/src/Checkbox/v2/styles.ts index 390f16921d..63ea8abf34 100644 --- a/packages/ui-checkbox/src/Checkbox/v2/styles.ts +++ b/packages/ui-checkbox/src/Checkbox/v2/styles.ts @@ -23,7 +23,8 @@ */ import type { CheckboxProps, CheckboxStyle } from './props' -import type { NewComponentTypes } from '@instructure/ui-themes' +import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' +import { calcSpacingFromShorthand } from '@instructure/emotion' /** * --- @@ -38,9 +39,10 @@ import type { NewComponentTypes } from '@instructure/ui-themes' */ const generateStyle = ( componentTheme: ReturnType, - props: CheckboxProps + props: CheckboxProps, + sharedTokens: SharedTokens ): CheckboxStyle => { - const { inline, size, variant } = props + const { inline, size, variant, margin } = props // toggleFullWidth calculation based on Toggle facade width (1.625rem * 1.5) and the marginInlineEnd (0.75rem) const toggleFullWidth = `calc(1.625rem * 1.5 + 0.75rem)` @@ -82,6 +84,11 @@ const generateStyle = ( label: 'checkbox', position: 'relative', width: '100%', + // Resolves spacing tokens (or custom CSS values) into a margin shorthand. + margin: calcSpacingFromShorthand(margin, { + ...sharedTokens.spacing, + ...sharedTokens.legacy.spacing + }), ...(inline && { display: 'inline-block', verticalAlign: 'middle', diff --git a/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx b/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx index 5e65888aac..e4f6a50faf 100644 --- a/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx +++ b/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx @@ -233,4 +233,16 @@ describe('', () => { expect(group).not.toHaveAttribute('aria-invalid') }) }) + + describe('margin prop', () => { + it('forwards margin to the field layout, resolving spacing tokens', async () => { + const { container } = await renderCheckboxGroup({ + margin: 'general.spaceMd' + }) + const layout = container.querySelector("[class$='-formFieldLayout']") + + // general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(layout!).marginLeft).toBe('12px') + }) + }) }) diff --git a/packages/ui-checkbox/src/CheckboxGroup/v2/README.md b/packages/ui-checkbox/src/CheckboxGroup/v2/README.md index bc35881340..7f306327cd 100644 --- a/packages/ui-checkbox/src/CheckboxGroup/v2/README.md +++ b/packages/ui-checkbox/src/CheckboxGroup/v2/README.md @@ -12,6 +12,7 @@ type: example --- CheckboxGroup examples}> ', () => { expect(axeCheck).toBe(true) }) + + describe('margin prop', () => { + it('forwards margin to the field layout, resolving spacing tokens', async () => { + const { container } = await render( + + + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + // general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(layout!).marginLeft).toBe('12px') + }) + }) }) diff --git a/packages/ui-form-field/src/FormFieldGroup/v2/README.md b/packages/ui-form-field/src/FormFieldGroup/v2/README.md index b0c2a37ab7..37275a6602 100644 --- a/packages/ui-form-field/src/FormFieldGroup/v2/README.md +++ b/packages/ui-form-field/src/FormFieldGroup/v2/README.md @@ -17,6 +17,7 @@ type: example rowSpacing="small" layout="inline" vAlign="middle" + margin="general.spaceMd 0" > ', () => { expect(axeCheck).toBe(true) }) }) + + describe('margin prop', () => { + it('resolves spacing tokens and custom CSS values', async () => { + const { container } = await render( +
+ + +
+ ) + const inputs = container.querySelectorAll("[class$='-radioInput']") + + // general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(inputs[0]!).marginLeft).toBe('12px') + expect(getComputedStyle(inputs[1]!).marginLeft).toBe('30px') + }) + }) }) diff --git a/packages/ui-radio-input/src/RadioInput/v2/index.tsx b/packages/ui-radio-input/src/RadioInput/v2/index.tsx index 9bfca6d3d2..324e013aba 100644 --- a/packages/ui-radio-input/src/RadioInput/v2/index.tsx +++ b/packages/ui-radio-input/src/RadioInput/v2/index.tsx @@ -58,6 +58,7 @@ const RadioInput = forwardRef( inline = false, context = 'success', readOnly = false, + margin, id: idProp, label, value, @@ -100,7 +101,8 @@ const RadioInput = forwardRef( hovered, readOnly, size, - variant + variant, + margin }, componentId: 'RadioInput', displayName: 'RadioInput' diff --git a/packages/ui-radio-input/src/RadioInput/v2/props.ts b/packages/ui-radio-input/src/RadioInput/v2/props.ts index 37ded5a9f8..149ca42a1b 100644 --- a/packages/ui-radio-input/src/RadioInput/v2/props.ts +++ b/packages/ui-radio-input/src/RadioInput/v2/props.ts @@ -25,7 +25,11 @@ import React from 'react' import type { InputHTMLAttributes } from 'react' import type { OtherHTMLAttributes } from '@instructure/shared-types' -import type { ComponentStyle, NewThemeOverrideProp } from '@instructure/emotion' +import type { + ComponentStyle, + NewThemeOverrideProp, + Spacing +} from '@instructure/emotion' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' import type { NewComponentTypes } from '@instructure/ui-themes' @@ -77,6 +81,12 @@ type RadioInputOwnProps = { * Sets the `display:inline-flex` in CSS */ inline?: boolean + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing onClick?: (event: React.MouseEvent) => void /** * Callback fired when the input fires a change event. @@ -118,6 +128,7 @@ const allowedProps: AllowedPropKeys = [ 'size', 'context', 'inline', + 'margin', 'onClick', 'onChange', 'inputRef' diff --git a/packages/ui-radio-input/src/RadioInput/v2/styles.ts b/packages/ui-radio-input/src/RadioInput/v2/styles.ts index ae03c2b3b6..7ac8b4be81 100644 --- a/packages/ui-radio-input/src/RadioInput/v2/styles.ts +++ b/packages/ui-radio-input/src/RadioInput/v2/styles.ts @@ -25,7 +25,10 @@ import type { RadioInputProps, RadioInputStyle } from './props' import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' import { boxShadowObjectsToCSSString } from '@instructure/ui-themes' -import { calcFocusOutlineStyles } from '@instructure/emotion' +import { + calcFocusOutlineStyles, + calcSpacingFromShorthand +} from '@instructure/emotion' type StyleParams = { disabled: RadioInputProps['disabled'] @@ -35,6 +38,7 @@ type StyleParams = { readOnly: RadioInputProps['readOnly'] size: RadioInputProps['size'] variant: RadioInputProps['variant'] + margin: RadioInputProps['margin'] } /** @@ -52,7 +56,16 @@ const generateStyle = ( params: StyleParams, sharedTokens: SharedTokens ): RadioInputStyle => { - const { disabled, inline, hovered, size, readOnly, variant, context } = params + const { + disabled, + inline, + hovered, + size, + readOnly, + variant, + context, + margin + } = params // 4*2 states: base, hover, disabled, readonly X none/selected const insetSizes = { @@ -272,6 +285,11 @@ const generateStyle = ( display: 'flex', alignItems: 'flex-start', width: inline ? 'auto' : '100%', + // Resolves spacing tokens (or custom CSS values) into a margin shorthand. + margin: calcSpacingFromShorthand(margin, { + ...sharedTokens.spacing, + ...sharedTokens.legacy.spacing + }), ...(inline && { display: 'inline-flex', verticalAlign: 'middle' diff --git a/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx b/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx index 86a9b93049..e2fb3c75ee 100644 --- a/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx +++ b/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx @@ -229,4 +229,23 @@ describe('', () => { expect(group).toHaveAttribute('aria-invalid', 'true') expect(group).toHaveAttribute('aria-errormessage', expect.anything()) }) + + describe('margin prop', () => { + it('forwards margin to the field layout, resolving spacing tokens', async () => { + const { container } = await render( + + + + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + // general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(layout!).marginLeft).toBe('12px') + }) + }) }) diff --git a/packages/ui-radio-input/src/RadioInputGroup/v2/README.md b/packages/ui-radio-input/src/RadioInputGroup/v2/README.md index ad51d97f5d..5a362a12a4 100644 --- a/packages/ui-radio-input/src/RadioInputGroup/v2/README.md +++ b/packages/ui-radio-input/src/RadioInputGroup/v2/README.md @@ -25,7 +25,7 @@ function Example () { console.log(value) } return ( - + {inputs.map(input => )} ) diff --git a/packages/ui-radio-input/src/RadioInputGroup/v2/props.ts b/packages/ui-radio-input/src/RadioInputGroup/v2/props.ts index 33ddd9556e..361b71365e 100644 --- a/packages/ui-radio-input/src/RadioInputGroup/v2/props.ts +++ b/packages/ui-radio-input/src/RadioInputGroup/v2/props.ts @@ -27,6 +27,7 @@ import React from 'react' import type { FormMessage } from '@instructure/ui-form-field/latest' import type { OtherHTMLAttributes } from '@instructure/shared-types' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' +import type { Spacing } from '@instructure/emotion' type RadioInputGroupOwnProps = { /** @@ -88,6 +89,13 @@ type RadioInputGroupOwnProps = { * Setting this to `true` adds and asterisk after the description (group label). It does not cause any behavioural change. */ isRequired?: boolean + + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing } type PropKeys = keyof RadioInputGroupOwnProps @@ -115,7 +123,8 @@ const allowedProps: AllowedPropKeys = [ 'variant', 'size', 'layout', - 'isRequired' + 'isRequired', + 'margin' ] export type { RadioInputGroupProps, RadioInputGroupState } diff --git a/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx b/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx index 6207c8919c..2e1c44fde3 100644 --- a/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx +++ b/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx @@ -389,4 +389,20 @@ describe('', () => { expect(messages).toHaveTextContent(message) }) }) + + it('forwards the margin prop to the field layout', async () => { + const { container } = await render( + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + // general.spaceMd (0.75rem) resolves to 12px + expect(getComputedStyle(layout!).marginLeft).toBe('12px') + }) }) diff --git a/packages/ui-range-input/src/RangeInput/v2/README.md b/packages/ui-range-input/src/RangeInput/v2/README.md index b036c25ba3..70a4c6e382 100644 --- a/packages/ui-range-input/src/RangeInput/v2/README.md +++ b/packages/ui-range-input/src/RangeInput/v2/README.md @@ -24,6 +24,7 @@ const Example = () => { max={100} min={0} size={size} + margin="general.spaceMd 0" /> diff --git a/packages/ui-range-input/src/RangeInput/v2/props.ts b/packages/ui-range-input/src/RangeInput/v2/props.ts index 90f28388d4..d9f17cbcfd 100644 --- a/packages/ui-range-input/src/RangeInput/v2/props.ts +++ b/packages/ui-range-input/src/RangeInput/v2/props.ts @@ -32,7 +32,11 @@ import type { FormFieldOwnProps, FormMessage } from '@instructure/ui-form-field/latest' -import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { + WithStyleProps, + ComponentStyle, + Spacing +} from '@instructure/emotion' import type { NewComponentTypes } from '@instructure/ui-themes' import type { InputHTMLAttributes } from 'react' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' @@ -88,6 +92,13 @@ type RangeInputOwnProps = { readOnly?: boolean + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing + /** * A function that provides a reference to the actual underlying input element */ @@ -139,6 +150,7 @@ const allowedProps: AllowedPropKeys = [ 'inline', 'disabled', 'readOnly', + 'margin', 'inputRef' ] diff --git a/packages/ui-text/src/Text/__tests__/Text.test.tsx b/packages/ui-text/src/Text/__tests__/Text.test.tsx index dae4573231..019f9e293a 100644 --- a/packages/ui-text/src/Text/__tests__/Text.test.tsx +++ b/packages/ui-text/src/Text/__tests__/Text.test.tsx @@ -49,4 +49,20 @@ describe('', () => { expect(text?.tagName).toBe('LI') }) + + describe('margin prop', () => { + it('resolves spacing tokens and custom CSS values', async () => { + const { container } = await render( +
+ A + B +
+ ) + const texts = container.querySelectorAll("[class$='-text']") + + // Text renders inline, so horizontal margins apply; general.spaceMd = 0.75rem = 12px + expect(getComputedStyle(texts[0]!).marginLeft).toBe('12px') + expect(getComputedStyle(texts[1]!).marginLeft).toBe('30px') + }) + }) }) diff --git a/packages/ui-text/src/Text/v2/README.md b/packages/ui-text/src/Text/v2/README.md index a7a1a3deed..0e282d464b 100644 --- a/packages/ui-text/src/Text/v2/README.md +++ b/packages/ui-text/src/Text/v2/README.md @@ -186,6 +186,21 @@ type: example
``` +### Margin + +Use the `margin` prop to add space around the component with spacing tokens or CSS-like shorthand. On the default inline `Text` only left/right margins render, so render it as a block element (`as="p"`) when you need vertical spacing. + +```js +--- +type: example +--- +
+ text + text + text +
+``` + ### DEPRECATED legacy values Multiple values from `size`, `weight` and `lineHeight` are deprecated, but still supported for backward compatibility reasons. Please only use the above listed, semantic values. diff --git a/packages/ui-text/src/Text/v2/props.ts b/packages/ui-text/src/Text/v2/props.ts index 6c546957b9..d3349ba3a3 100644 --- a/packages/ui-text/src/Text/v2/props.ts +++ b/packages/ui-text/src/Text/v2/props.ts @@ -27,7 +27,11 @@ import type { AsElementType, OtherHTMLAttributes } from '@instructure/shared-types' -import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { + WithStyleProps, + ComponentStyle, + Spacing +} from '@instructure/emotion' import type { NewComponentTypes } from '@instructure/ui-themes' type TextOwnProps = { @@ -109,6 +113,12 @@ type TextOwnProps = { */ weight?: 'normal' | 'light' | 'bold' | 'weightRegular' | 'weightImportant' wrap?: 'normal' | 'break-word' + /** + * Valid values are `0`, `none`, `auto`, and Spacing token values, + * see https://instructure.design/layout-spacing. Apply these values via + * familiar CSS-like shorthand. For example, `margin="general.spaceMd auto"`. + */ + margin?: Spacing children?: React.ReactNode } @@ -133,7 +143,8 @@ const allowedProps: AllowedPropKeys = [ 'transform', 'variant', 'weight', - 'wrap' + 'wrap', + 'margin' ] export type { TextProps, TextStyle } diff --git a/packages/ui-text/src/Text/v2/styles.ts b/packages/ui-text/src/Text/v2/styles.ts index bc46177f5d..1ddbdeb1f7 100644 --- a/packages/ui-text/src/Text/v2/styles.ts +++ b/packages/ui-text/src/Text/v2/styles.ts @@ -23,8 +23,9 @@ */ import type { TextProps, TextStyle } from './props' -import type { NewComponentTypes } from '@instructure/ui-themes' +import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' import type { TokenTypographyValueInst } from '@instructure/ui-themes' +import { calcSpacingFromShorthand } from '@instructure/emotion' type StyleParams = { color: TextProps['color'] @@ -36,6 +37,7 @@ type StyleParams = { variant: TextProps['variant'] weight: TextProps['weight'] wrap: TextProps['wrap'] + margin: TextProps['margin'] } /** @@ -45,11 +47,13 @@ type StyleParams = { * Generates the style object from the theme and provided additional information * @param componentTheme The theme variable object. * @param params Additional parameters to customize the style. + * @param sharedTokens Shared token object that stores common values for the theme. * @return The final style object, which will be used in the component */ const generateStyle = ( componentTheme: ReturnType, - params: StyleParams + params: StyleParams, + sharedTokens: SharedTokens ): TextStyle => { const { color, @@ -60,7 +64,8 @@ const generateStyle = ( size, variant, weight, - wrap + wrap, + margin } = params const variants: Record< @@ -220,6 +225,11 @@ const generateStyle = ( label: 'text', fontFamily: componentTheme.fontFamily, ...baseStyles, + // Resolves spacing tokens (or custom CSS values) into a margin shorthand. + margin: calcSpacingFromShorthand(margin, { + ...sharedTokens.spacing, + ...sharedTokens.legacy.spacing + }), // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because // of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)