diff --git a/docs/contributor-docs/v10-upgrade-guide.md b/docs/contributor-docs/v10-upgrade-guide.md index 2f9590c4d2..085a462f05 100644 --- a/docs/contributor-docs/v10-upgrade-guide.md +++ b/docs/contributor-docs/v10-upgrade-guide.md @@ -1,7 +1,7 @@ --- title: Upgrade Guide for Version 10.0 category: Guides -order: 7 +order: 98 --- # Upgrade Guide for Version 10 diff --git a/docs/guides/layout-spacing.md b/docs/guides/layout-spacing.md new file mode 100644 index 0000000000..93a928bc53 --- /dev/null +++ b/docs/guides/layout-spacing.md @@ -0,0 +1,92 @@ +--- +title: Layout Spacing +category: Guides +order: 8 +--- + +# Layout Spacing + +Our design system provides a set of spacing tokens for consistent layouts and components. Some tokens share values but should be used semantically. For instance, while both `space12` and `buttons` are 12px, `buttons` should be used for spacing between buttons. + +## Tokens + +| Key | Value | +|--------------------|--------| +| space0 | 0px | +| space2 | 2px | +| space4 | 4px | +| space8 | 8px | +| space12 | 12px | +| space16 | 16px | +| space24 | 24px | +| space36 | 36px | +| space48 | 48px | +| space60 | 60px | +| sections | 36px | +| sectionElements | 24px | +| trayElements | 24px | +| modalElements | 24px | +| moduleElements | 16px | +| paddingCardLarge | 24px | +| paddingCardMedium | 16px | +| paddingCardSmall | 12px | +| selects | 16px | +| textAreas | 16px | +| inputFields | 16px | +| checkboxes | 16px | +| radios | 16px | +| toggles | 16px | +| buttons | 12px | +| tags | 12px | +| statusIndicators | 12px | +| dataPoints | 12px | + +## Applying Spacing + +There are three main ways to apply spacing in our component library: + +### 1. Using the `margin` Prop + +Most components in the library support a `margin` prop that works similarly to the CSS margin property. You can specify a single value or fine-tune individual margins (e.g., top, right, bottom, left). + +```ts +--- +type: example +--- +
+ + +
+``` + +### 2. Using a Container Component with the `gap` Prop + +For layouts, container components like `Flex` and `Grid` can be used with the gap prop to manage spacing between child elements. + +```ts +--- +type: example +--- + + + + +``` + +### 3. Importing Values from the Theme + +If you need to directly reference spacing values, you can import them from the theme. This approach is useful for applying spacing in inline styles or custom components. + +```ts +--- +type: code +--- +// import the canvas theme +import canvas from '@instructure/ui-themes' + +// use spacing values +
+ + +
+``` diff --git a/packages/emotion/src/styleUtils/ThemeablePropValues.ts b/packages/emotion/src/styleUtils/ThemeablePropValues.ts index 9becdbcf47..63dc278c08 100644 --- a/packages/emotion/src/styleUtils/ThemeablePropValues.ts +++ b/packages/emotion/src/styleUtils/ThemeablePropValues.ts @@ -84,7 +84,7 @@ const ThemeablePropValues = { medium: 'medium', large: 'large', xLarge: 'x-large', - xxLarge: 'xx-large' + xxLarge: 'xx-large', } } as const diff --git a/packages/ui-date-input/src/DateInput2/index.tsx b/packages/ui-date-input/src/DateInput2/index.tsx index 3a26f5a6c1..95f183a651 100644 --- a/packages/ui-date-input/src/DateInput2/index.tsx +++ b/packages/ui-date-input/src/DateInput2/index.tsx @@ -152,7 +152,7 @@ const DateInput2 = ({ placeholder, dateFormat, onRequestValidateDate, - // margin, TODO enable this prop + margin, ...rest }: DateInput2Props) => { const localeContext = useContext(ApplyLocaleContext) @@ -277,7 +277,6 @@ const DateInput2 = ({ return ( void - // margin?: Spacing // TODO enable this prop + + /** + * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](/#layout-spacing). + */ + margin?: Spacing } type PropKeys = keyof DateInput2OwnProps @@ -195,7 +200,8 @@ const propTypes: PropValidators = { timezone: PropTypes.string, withYearPicker: PropTypes.object, dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - onRequestValidateDate: PropTypes.func + onRequestValidateDate: PropTypes.func, + margin: PropTypes.string, } export type { DateInput2Props } diff --git a/packages/ui-form-field/src/FormField/index.tsx b/packages/ui-form-field/src/FormField/index.tsx index b97bd66202..a5cfe3cfa3 100644 --- a/packages/ui-form-field/src/FormField/index.tsx +++ b/packages/ui-form-field/src/FormField/index.tsx @@ -70,6 +70,7 @@ class FormField extends Component { as="label" htmlFor={this.props.id} elementRef={this.handleRef} + margin={this.props.margin} /> ) } diff --git a/packages/ui-form-field/src/FormField/props.ts b/packages/ui-form-field/src/FormField/props.ts index 81dc172264..11b016ea17 100644 --- a/packages/ui-form-field/src/FormField/props.ts +++ b/packages/ui-form-field/src/FormField/props.ts @@ -32,6 +32,7 @@ import type { PropValidators } from '@instructure/shared-types' import type { FormMessage } from '../FormPropTypes' +import type { Spacing } from '@instructure/emotion' type FormFieldOwnProps = { label: React.ReactNode @@ -62,6 +63,11 @@ type FormFieldOwnProps = { * provides a reference to the underlying html root element */ elementRef?: (element: Element | null) => void + + /** + * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](/#layout-spacing). + */ + margin?: Spacing } type PropKeys = keyof FormFieldOwnProps @@ -82,7 +88,8 @@ const propTypes: PropValidators = { vAlign: PropTypes.oneOf(['top', 'middle', 'bottom']), width: PropTypes.string, inputContainerRef: PropTypes.func, - elementRef: PropTypes.func + elementRef: PropTypes.func, + margin: PropTypes.string, } const allowedProps: AllowedPropKeys = [ @@ -97,7 +104,8 @@ const allowedProps: AllowedPropKeys = [ 'vAlign', 'width', 'inputContainerRef', - 'elementRef' + 'elementRef', + 'margin' ] export type { FormFieldOwnProps, FormFieldProps } diff --git a/packages/ui-form-field/src/FormFieldLayout/index.tsx b/packages/ui-form-field/src/FormFieldLayout/index.tsx index cf7c810c6c..95f2c21aba 100644 --- a/packages/ui-form-field/src/FormFieldLayout/index.tsx +++ b/packages/ui-form-field/src/FormFieldLayout/index.tsx @@ -45,6 +45,7 @@ import generateStyle from './styles' import { propTypes, allowedProps } from './props' import type { FormFieldLayoutProps } from './props' +import generateComponentTheme from './theme' /** --- @@ -52,7 +53,7 @@ parent: FormField --- **/ @withDeterministicId() -@withStyle(generateStyle, null) +@withStyle(generateStyle, generateComponentTheme) class FormFieldLayout extends Component { static readonly componentId = 'FormFieldLayout' diff --git a/packages/ui-form-field/src/FormFieldLayout/props.ts b/packages/ui-form-field/src/FormFieldLayout/props.ts index 837443e7e5..8b3313f7da 100644 --- a/packages/ui-form-field/src/FormFieldLayout/props.ts +++ b/packages/ui-form-field/src/FormFieldLayout/props.ts @@ -31,7 +31,7 @@ import type { OtherHTMLAttributes, PropValidators } from '@instructure/shared-types' -import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { WithStyleProps, ComponentStyle, Spacing } from '@instructure/emotion' import type { FormMessage } from '../FormPropTypes' import type { WithDeterministicIdProps } from '@instructure/ui-react-utils' @@ -68,6 +68,11 @@ type FormFieldLayoutOwnProps = { */ elementRef?: (element: Element | null) => void isGroup?: boolean + + /** + * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](/#layout-spacing). + */ + margin?: Spacing } type PropKeys = keyof FormFieldLayoutOwnProps @@ -97,7 +102,8 @@ const propTypes: PropValidators = { width: PropTypes.string, inputContainerRef: PropTypes.func, elementRef: PropTypes.func, - isGroup: PropTypes.bool + isGroup: PropTypes.bool, + margin: PropTypes.string, } const allowedProps: AllowedPropKeys = [ @@ -112,7 +118,8 @@ const allowedProps: AllowedPropKeys = [ 'labelAlign', 'width', 'inputContainerRef', - 'elementRef' + 'elementRef', + 'margin' // added vAlign because FormField and FormFieldGroup passes it, but not adding // it to allowedProps to prevent it from getting passed through accidentally diff --git a/packages/ui-form-field/src/FormFieldLayout/styles.ts b/packages/ui-form-field/src/FormFieldLayout/styles.ts index 542ad8e95d..5b361b25a5 100644 --- a/packages/ui-form-field/src/FormFieldLayout/styles.ts +++ b/packages/ui-form-field/src/FormFieldLayout/styles.ts @@ -35,10 +35,11 @@ import type { FormFieldLayoutProps, FormFieldLayoutStyle } from './props' * @return {Object} The final style object, which will be used in the component */ const generateStyle = ( - _componentTheme: null, + componentTheme: any, props: FormFieldLayoutProps ): FormFieldLayoutStyle => { - const { inline } = props + const { inline, margin } = props + const { spacing } = componentTheme return { groupErrorMessage: { @@ -49,7 +50,7 @@ const generateStyle = ( all: 'initial', border: '0', padding: '0', - margin: '0', + margin: margin ? spacing[margin] : '0', minWidth: '0', direction: 'inherit', textAlign: 'start', diff --git a/packages/ui-form-field/src/FormFieldLayout/theme.ts b/packages/ui-form-field/src/FormFieldLayout/theme.ts new file mode 100644 index 0000000000..6a7a7a6807 --- /dev/null +++ b/packages/ui-form-field/src/FormFieldLayout/theme.ts @@ -0,0 +1,33 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * 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. + */ + +import type { Theme } from '@instructure/ui-themes' + +const generateComponentTheme = (theme: Theme): any => { + const { spacing } = theme + + return { spacing } +} + +export default generateComponentTheme diff --git a/packages/ui-number-input/src/NumberInput/index.tsx b/packages/ui-number-input/src/NumberInput/index.tsx index 4c40a1e2f6..6b1e4bc72f 100644 --- a/packages/ui-number-input/src/NumberInput/index.tsx +++ b/packages/ui-number-input/src/NumberInput/index.tsx @@ -238,7 +238,8 @@ class NumberInput extends Component { value, width, styles, - allowStringValue + allowStringValue, + margin, } = this.props const { interaction } = this @@ -268,6 +269,7 @@ class NumberInput extends Component { inline={display === 'inline-block'} id={this.id} elementRef={this.handleRef} + margin={margin} > = { onKeyDown: PropTypes.func, inputMode: PropTypes.oneOf(['numeric', 'decimal', 'tel']), textAlign: PropTypes.oneOf(['start', 'center']), - allowStringValue: PropTypes.bool + allowStringValue: PropTypes.bool, + margin: PropTypes.string, } const allowedProps: AllowedPropKeys = [ @@ -250,7 +255,8 @@ const allowedProps: AllowedPropKeys = [ 'onKeyDown', 'inputMode', 'textAlign', - 'allowStringValue' + 'allowStringValue', + 'margin', ] export type { diff --git a/packages/ui-text-area/src/TextArea/index.tsx b/packages/ui-text-area/src/TextArea/index.tsx index 942b5c0319..a65f28956e 100644 --- a/packages/ui-text-area/src/TextArea/index.tsx +++ b/packages/ui-text-area/src/TextArea/index.tsx @@ -315,7 +315,8 @@ class TextArea extends Component { maxHeight, textareaRef, resize, - styles + styles, + margin, } = this.props const props = omitProps(this.props, TextArea.allowedProps) @@ -380,6 +381,7 @@ class TextArea extends Component { elementRef={(el) => { this.ref = el }} + margin={margin} >
) => void + + /** + * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](/#layout-spacing). + */ + margin?: Spacing } type PropKeys = keyof TextAreaOwnProps @@ -153,7 +158,8 @@ const propTypes: PropValidators = { textareaRef: PropTypes.func, defaultValue: PropTypes.string, value: controllable(PropTypes.string), - onChange: PropTypes.func + onChange: PropTypes.func, + margin: PropTypes.string, } const allowedProps: AllowedPropKeys = [ @@ -175,7 +181,8 @@ const allowedProps: AllowedPropKeys = [ 'textareaRef', 'defaultValue', 'value', - 'onChange' + 'onChange', + 'margin', ] export type { TextAreaProps, TextAreaStyle } diff --git a/packages/ui-text-input/src/TextInput/index.tsx b/packages/ui-text-input/src/TextInput/index.tsx index 137484dc0a..b9d808133b 100644 --- a/packages/ui-text-input/src/TextInput/index.tsx +++ b/packages/ui-text-input/src/TextInput/index.tsx @@ -357,6 +357,7 @@ class TextInput extends Component { inputContainerRef={inputContainerRef} layout={this.props.layout} elementRef={this.handleRef} + margin={this.props.margin} > {renderBeforeOrAfter ? ( diff --git a/packages/ui-text-input/src/TextInput/props.ts b/packages/ui-text-input/src/TextInput/props.ts index ab3c64ba03..d6421554b0 100644 --- a/packages/ui-text-input/src/TextInput/props.ts +++ b/packages/ui-text-input/src/TextInput/props.ts @@ -34,7 +34,7 @@ import type { PropValidators, TextInputTheme } from '@instructure/shared-types' -import type { WithStyleProps, ComponentStyle } from '@instructure/emotion' +import type { WithStyleProps, ComponentStyle, Spacing } from '@instructure/emotion' import type { InteractionType, WithDeterministicIdProps @@ -172,6 +172,11 @@ type TextInputOwnProps = { * Callback fired when input receives focus. */ onFocus?: (event: React.FocusEvent) => void + + /** + * Margin around the component. Accepts a `Spacing` token. See token values and example usage in [this guide](/#layout-spacing). + */ + margin?: Spacing } type PropKeys = keyof TextInputOwnProps @@ -223,7 +228,8 @@ const propTypes: PropValidators = { renderAfterInput: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), onChange: PropTypes.func, onBlur: PropTypes.func, - onFocus: PropTypes.func + onFocus: PropTypes.func, + margin: PropTypes.string, } const allowedProps: AllowedPropKeys = [ @@ -249,7 +255,8 @@ const allowedProps: AllowedPropKeys = [ 'renderAfterInput', 'onChange', 'onBlur', - 'onFocus' + 'onFocus', + 'margin', ] type TextInputState = { diff --git a/packages/ui-themes/src/sharedThemeTokens/spacing.ts b/packages/ui-themes/src/sharedThemeTokens/spacing.ts index 1409d4d175..1301c179ae 100644 --- a/packages/ui-themes/src/sharedThemeTokens/spacing.ts +++ b/packages/ui-themes/src/sharedThemeTokens/spacing.ts @@ -25,6 +25,7 @@ import { Spacing } from '@instructure/shared-types' const spacing: Spacing = Object.freeze({ + // legacy spacing tokens: xxxSmall: '0.125rem', // 2px xxSmall: '0.375rem', // 6px xSmall: '0.5rem', // 8px @@ -33,7 +34,37 @@ const spacing: Spacing = Object.freeze({ medium: '1.5rem', // 24px large: '2.25rem', // 36px xLarge: '3rem', // 48px - xxLarge: '3.75rem' // 60px + xxLarge: '3.75rem', // 60px + + // new spacing tokens: + space0: '0px', + space2: '2px', + space4: '4px', + space8: '8px', + space12: '12px', + space16: '16px', + space24: '24px', + space36: '36px', + space48: '48px', + space60: '60px', + sections: '36px', + sectionElements: '24px', + trayElements: '24px', + modalElements: '24px', + moduleElements: '16px', + paddingCardLarge: '24px', + paddingCardMedium: '16px', + paddingCardSmall: '12px', + selects: '16px', + textareas: '16px', + inputFields: '16px', + checkboxes: '16px', + radios: '16px', + toggles: '16px', + buttons: '12px', + tags: '12px', + statusIndicators: '12px', + dataPoints: '12px', } as const) export default spacing diff --git a/todo-add-margin-componentlist.md b/todo-add-margin-componentlist.md new file mode 100644 index 0000000000..408d8919a0 --- /dev/null +++ b/todo-add-margin-componentlist.md @@ -0,0 +1,77 @@ +- [] Alert +- [] AppNav +- [] Avatar +- [] Badge +- [] Billboard +- [] Breadcrumb +- [] Button +- [] Byline +- [x] Calendar +- [x] Checkbox +- [x] CheckboxGroup +- [] CloseButton +- [] ColorContrast +- [x] ColorIndicator +- [x] ColorMixer +- [x] ColorPicker +- [x] ColorPreset +- [] CondensedButton +- [] ContextView +- [x] DateInput +- [x] DateInput2 +- [x] DateTimeInput +- [] DrawerLayout +- [?] Drilldown +- [x] Editable +- [] FileDrop +- [] Flex +- [x] FormField +- [x] FormFieldGroup +- [x] Grid +- [] Heading +- [] IconButton +- [] Img +- [] InlineList +- [x] InPlaceEdit +- [] Link +- [] List +- [x] Menu +- [x] Metric +- [x] MetricGroup +- [] Modal +- [x] NumberInput +- [x] Options +- [] Overlay +- [] Pages +- [] Pagination +- [] Pill +- [] Popover +- [] ProgressBar +- [] ProgressCircle +- [x] RadioInput +- [x] RadioInputGroup +- [x] RangeInput +- [] Rating +- [?] Responsive +- [x] Select +- [x] Selectable +- [] SideNavBar +- [x] SimpleSelect +- [x] SourceCodeEditor +- [] Spinner +- [] Table +- [] Tabs +- [] Tag +- [x] Text +- [x] TextArea +- [x] TextInput +- [x] TimeSelect +- [x] ToggleButton +- [x] ToggleDetails +- [x] ToggleGroup +- [] Tooltip +- [] TopNavBar +- [] Tray +- [x] TreeBrowser +- [x] TruncateText +- [] View