diff --git a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx index f2acf644cc..43c754205b 100644 --- a/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx +++ b/packages/ui-buttons/src/ToggleButton/__tests__/ToggleButton.test.tsx @@ -195,4 +195,21 @@ describe('', () => { expect(onClick).toHaveBeenCalledTimes(1) }) }) + + describe('margin prop', () => { + it('forwards margin to the button, resolving spacing tokens', () => { + render( + + ) + const button = screen.getByRole('button') + + expect(getComputedStyle(button).marginTop).toEqual('0.75rem') + }) + }) }) 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 45e6d94b9b..349925403b 100644 --- a/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx +++ b/packages/ui-checkbox/src/Checkbox/__tests__/Checkbox.test.tsx @@ -320,4 +320,28 @@ describe('', () => { expect(input).not.toHaveAttribute('aria-labelledby') }) }) + + describe('margin prop', () => { + it('resolves spacing tokens, custom CSS values, and shorthand', () => { + const { container } = render( +
+ + + +
+ ) + const boxes = container.querySelectorAll("[class$='-checkbox']") + + const token = getComputedStyle(boxes[0]!) + expect(token.marginTop).toEqual('0.75rem') // general.spaceMd + expect(token.marginLeft).toEqual('0.75rem') + + const custom = getComputedStyle(boxes[1]!) + expect(custom.marginTop).toEqual('30px') + + const shorthand = getComputedStyle(boxes[2]!) + expect(shorthand.marginTop).toEqual('1rem') // general.spaceLg + expect(shorthand.marginRight).toEqual('auto') + }) + }) }) 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 e3957fca36..34588cf307 100644 --- a/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx +++ b/packages/ui-checkbox/src/CheckboxGroup/__tests__/CheckboxGroup.test.tsx @@ -232,4 +232,13 @@ describe('', () => { expect(group).not.toHaveAttribute('aria-invalid') }) }) + + describe('margin prop', () => { + it('forwards margin to the field layout, resolving spacing tokens', () => { + const { container } = renderCheckboxGroup({ margin: 'general.spaceMd' }) + const layout = container.querySelector("[class$='-formFieldLayout']") + + expect(getComputedStyle(layout!).marginTop).toEqual('0.75rem') + }) + }) }) 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', () => { + const { container } = render( + + + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + expect(getComputedStyle(layout!).marginTop).toEqual('0.75rem') + }) + }) }) 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, custom CSS values, and shorthand', () => { + const { container } = render( +
+ + + +
+ ) + const inputs = container.querySelectorAll("[class$='-radioInput']") + + const token = getComputedStyle(inputs[0]!) + expect(token.marginTop).toEqual('0.75rem') // general.spaceMd + expect(token.marginLeft).toEqual('0.75rem') + + const custom = getComputedStyle(inputs[1]!) + expect(custom.marginTop).toEqual('30px') + + const shorthand = getComputedStyle(inputs[2]!) + expect(shorthand.marginTop).toEqual('1rem') // general.spaceLg + expect(shorthand.marginRight).toEqual('auto') + }) + }) }) 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 140ea275ff..832531e618 100644 --- a/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx +++ b/packages/ui-radio-input/src/RadioInputGroup/__tests__/RadioInputGroup.test.tsx @@ -230,4 +230,22 @@ 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', () => { + const { container } = render( + + + + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + expect(getComputedStyle(layout!).marginTop).toEqual('0.75rem') + }) + }) }) 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 7f8fcb8959..b409b27f55 100644 --- a/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx +++ b/packages/ui-range-input/src/RangeInput/__tests__/RangeInput.test.tsx @@ -266,4 +266,21 @@ describe('', () => { expect(output).toHaveTextContent('40%') }) }) + + describe('margin prop', () => { + it('forwards margin to the field layout, resolving spacing tokens', () => { + const { container } = render( + + ) + const layout = container.querySelector("[class$='-formFieldLayout']") + + expect(getComputedStyle(layout!).marginTop).toEqual('0.75rem') + }) + }) }) diff --git a/packages/ui-range-input/src/RangeInput/v2/README.md b/packages/ui-range-input/src/RangeInput/v2/README.md index f17e15752a..0e2d6654a2 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 bf683672b4..23cf05988f 100644 --- a/packages/ui-text/src/Text/__tests__/Text.test.tsx +++ b/packages/ui-text/src/Text/__tests__/Text.test.tsx @@ -49,4 +49,28 @@ describe('', () => { expect(text?.tagName).toBe('LI') }) + + describe('margin prop', () => { + it('resolves spacing tokens, custom CSS values, and shorthand', () => { + const { container } = render( +
+ A + B + C +
+ ) + const texts = container.querySelectorAll("[class$='-text']") + + const token = getComputedStyle(texts[0]!) + expect(token.marginTop).toEqual('0.75rem') // general.spaceMd + expect(token.marginLeft).toEqual('0.75rem') + + const custom = getComputedStyle(texts[1]!) + expect(custom.marginTop).toEqual('30px') + + const shorthand = getComputedStyle(texts[2]!) + expect(shorthand.marginTop).toEqual('1rem') // general.spaceLg + expect(shorthand.marginRight).toEqual('auto') + }) + }) }) 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 aca5546740..2046742182 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 = { @@ -106,6 +110,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 } @@ -130,7 +140,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)