MM-68654 Add Button to the Component Library (#36412)

* Add Button to Component Library

* Fix key warnings in Component Library

* Add grid of Buttons to match Figma

* Fix trailingIcon default
This commit is contained in:
Harrison Healey 2026-05-06 15:36:18 -04:00 committed by GitHub
parent 7f161bb24c
commit c1ddd77481
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 272 additions and 42 deletions

View file

@ -0,0 +1,200 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useMemo} from 'react';
import glyphMap from '@mattermost/compass-icons/components';
import {Button} from '@mattermost/shared/components/button';
import {useBooleanProp, useDropdownProp, useStringProp} from './hooks';
import {buildComponent} from './utils';
const propPossibilities = {};
const iconValues = [''].concat(Object.keys(glyphMap));
const emphasisValues = ['primary', 'secondary', 'tertiary', 'quaternary'];
const sizeValues = ['xs', 'sm', 'md', 'lg'];
const variantValues = ['', 'destructive'];
type Props = {
backgroundClass: string;
};
export default function ButtonComponentLibrary({backgroundClass}: Props) {
const [label, labelSelector] = useStringProp('label', 'Label', false);
const [leadingIcon, leadingIconPossibilities, leadingIconSelector] = useDropdownProp('leadingIcon', 'mattermost', iconValues, false);
const [trailingIcon, trailingIconPossibilities, trailingIconSelector] = useDropdownProp('trailingIcon', '', iconValues, false);
const [emphasis, emphasisPossibilities, emphasisSelector] = useDropdownProp('emphasis', 'primary', emphasisValues, true);
const [size, sizePossibilities, sizeSelector] = useDropdownProp('size', 'md', sizeValues, true);
const [variant, variantPossibilities, variantSelector] = useDropdownProp('variant', '', variantValues, true);
const [disabled, disabledSelector] = useBooleanProp('disabled', false);
const children = useMemo(() => (
<>
{leadingIcon?.leadingIcon ? <i className={classNames('icon', `icon-${leadingIcon.leadingIcon}`)}/> : null}
{label.label}
{trailingIcon?.trailingIcon ? <i className={classNames('icon', `icon-${trailingIcon.trailingIcon}`)}/> : null}
</>
), [label, leadingIcon, trailingIcon]);
const components = useMemo(
() => buildComponent(
Button,
propPossibilities,
[
emphasisPossibilities,
leadingIconPossibilities,
sizePossibilities,
trailingIconPossibilities,
variantPossibilities,
], [
{children},
emphasis,
size,
variant,
disabled,
],
),
[
children,
disabled,
emphasis,
emphasisPossibilities,
leadingIconPossibilities,
size,
sizePossibilities,
trailingIconPossibilities,
variant,
variantPossibilities,
],
);
return (
<>
{labelSelector}
{leadingIconSelector}
{trailingIconSelector}
<hr/>
{emphasisSelector}
{sizeSelector}
{variantSelector}
<hr/>
{disabledSelector}
<div className={classNames('clWrapper', backgroundClass)}>{components}</div>
<ButtonGrid/>
</>
);
}
function ButtonGrid() {
const sizes = ['md', 'xs', 'sm', 'lg'] as const;
const variants = ['', 'destructive', 'inverted'] as const;
const states = ['default', 'hover', 'active', 'focus', 'disabled'] as const;
const emphasisLevels = ['primary', 'secondary', 'tertiary', 'quaternary'] as const;
const rows = [];
for (const size of sizes) {
for (const variant of variants) {
for (const state of states) {
const row = [];
if (variant === '' && state === 'default') {
const sizeLabels = {md: 'medium', xs: 'x-small', sm: 'small', lg: 'large'} as const;
row.push(
<th
key='size'
scope='row'
>
{sizeLabels[size]}
</th>,
);
} else {
row.push(
<th key='size'/>,
);
}
if (state === 'default') {
row.push(
<th
key='variant'
scope='row'
>
{variant}
</th>,
);
} else {
row.push(
<th key='variant'/>,
);
}
row.push(
<th
key='state'
scope='row'
>
{state}
</th>,
);
let stateClassName = '';
if (state === 'hover' || state === 'active' || state === 'focus') {
stateClassName = `btn-force-${state}`;
}
for (const emphasis of emphasisLevels) {
row.push(
<td
key={emphasis}
className={classNames({inverted: variant === 'inverted'})}
>
<Button
emphasis={emphasis}
size={size}
variant={variant}
className={stateClassName}
disabled={state === 'disabled'}
>
{'Button'}
</Button>
</td>,
);
}
rows.push(
<tr key={`${size}-${variant}-${state}`} >
{row}
</tr>,
);
}
}
}
return (
<table className='clWrapper clTable'>
<thead>
<tr>
<th colSpan={3}/>
{emphasisLevels.map((emphasis) => (
<th
key={emphasis}
scope='col'
>
{emphasis}
</th>
))}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
);
}

View file

@ -6,6 +6,7 @@
.clWrapper {
width: auto;
padding: 25px;
margin-bottom: 20px;
}
.clCenterBackground {
@ -15,3 +16,26 @@
.clSidebarBackground {
background-color: var(--sidebar-bg);
}
.clTable {
width: 100%;
max-width: 970px;
background-color: var(--center-channel-bg);
th {
text-transform: capitalize;
}
tr {
background-color: var(--center-channel-bg);
}
th, td {
padding: 12px 10px;
text-align: center;
&.inverted {
background-color: var(--sidebar-bg);
}
}
}

View file

@ -8,11 +8,13 @@ import {Preferences} from 'mattermost-redux/constants';
import {applyTheme} from 'utils/utils';
import ButtonComponentLibrary from './button.cl';
import SectionNoticeComponentLibrary from './section_notice.cl';
import './component_library.scss';
const componentMap = {
Button: ButtonComponentLibrary,
'Section Notice': SectionNoticeComponentLibrary,
};

View file

@ -29,13 +29,17 @@ function buildPropString(inputProps: {[x: string]: any}) {
return undefined;
}
const result = [(<>{'PROPS: '}</>)];
const result = [(<React.Fragment key='propsTitle'>{'PROPS: '}</React.Fragment>)];
propKeys.forEach((v) => {
result.push((<><b>{v}</b>{`: ${inputProps[v]}, `}</>));
result.push((<React.Fragment key={v}><b>{v}</b>{`: ${inputProps[v]}, `}</React.Fragment >));
});
return result;
}
function buildPropValueKey(inputProps: {[x: string]: any}) {
return Object.entries(inputProps).map(([key, value]) => `${key}:${value}`).join('-');
}
export function buildComponent(
Component: React.ComponentType<any>,
propPossibilities: {[x: string]: any[]},
@ -66,13 +70,13 @@ export function buildComponent(
propsVariations.forEach((v) => {
const propString = buildPropString(v);
res.push(
<>
<React.Fragment key={buildPropValueKey(v)}>
{Boolean(propString) && <p>{propString}</p>}
<Component
{...builtSetProps}
{...v}
/>
</>,
</React.Fragment>,
);
});
return res;

View file

@ -5,7 +5,7 @@
border: none;
background: transparent;
&:focus {
&:focus, &.btn-force-focus {
outline: 0;
text-decoration: none;
}
@ -16,7 +16,7 @@
}
&:hover,
&:active {
&:active, &.btn-force-active {
text-decoration: none;
}
}
@ -62,12 +62,12 @@ button {
background-color: transparent;
color: rgba(var(--center-channel-color-rgb), var(--icon-opacity));
&:hover {
&:hover, &.btn-force-hover {
background-color: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), var(--icon-opacity-hover));
}
&:active {
&:active, &.btn-force-active {
background-color: rgba(var(--button-bg-rgb), 0.08);
color: rgba(var(--button-bg-rgb), 1);
}
@ -135,7 +135,7 @@ button {
box-shadow: none;
}
&:active {
&:active, &.btn-force-active {
box-shadow: none;
}
@ -203,9 +203,9 @@ button {
background: transparent;
color: rgba(var(--button-bg-rgb), 1);
&:hover,
&:focus,
&:active {
&:hover, &.btn-force-hover,
&:focus, &.btn-force-focus,
&:active, &.btn-force-active {
text-decoration: underline;
}
}
@ -217,16 +217,16 @@ button {
color: rgb(var(--button-color-rgb)) !important;
// These hover and active values are for things outside the app__body, the correct theme styles for the primary button are applied in utils.jsx
&:hover {
&:hover, &.btn-force-hover {
background-color: #1a51c8;
}
&:active,
&:focus {
&:active, &.btn-force-active,
&:focus, &.btn-force-focus {
background-color: #184ab6;
}
&:disabled,
&:disabled, &.btn-force-disabled,
&:disabled:hover,
&:disabled:active {
background: rgba(var(--center-channel-color-rgb), 0.08);
@ -238,11 +238,11 @@ button {
background-color: var(--online-indicator);
color: var(--button-color-rgb);
&:hover {
&:hover, &.btn-force-hover {
background-color: var(--online-indicator);
}
&:active {
&:active, &.btn-force-active {
background-color: var(--online-indicator);
}
}
@ -258,21 +258,21 @@ button {
background: transparent;
color: var(--error-text);
&:hover {
&:hover, &.btn-force-hover {
border-color: currentColor;
background-color: rgba(var(--error-text-color-rgb), 0.08);
color: var(--error-text);
}
&:active,
&:focus {
&:active, &.btn-force-active,
&:focus, &.btn-force-focus {
border-color: currentColor;
background-color: rgba(var(--error-text-color-rgb), 0.16);
color: var(--error-text);
}
}
&:disabled,
&:disabled, &.btn-force-disabled,
&:disabled:hover,
&:disabled:active {
border-color: rgba(var(--center-channel-color-rgb), 0.32);
@ -280,11 +280,11 @@ button {
color: rgba(var(--center-channel-color-rgb), 0.32) !important;
}
&:hover {
&:hover, &.btn-force-hover {
background-color: rgb(var(--button-bg-rgb), 0.08);
}
&:active {
&:active, &.btn-force-active {
background-color: rgb(var(--button-bg-rgb), 0.16);
}
}
@ -293,16 +293,16 @@ button {
background: rgba(var(--button-bg-rgb), 0.08);
color: rgb(var(--button-bg-rgb));
&:hover {
&:hover, &.btn-force-hover {
background-color: rgb(var(--button-bg-rgb), 0.12);
}
&:active {
&:active, &.btn-force-active {
background-color: rgb(var(--button-bg-rgb), 0.16);
outline: none;
}
&:disabled,
&:disabled, &.btn-force-disabled,
&:disabled:hover,
&:disabled:active {
background: rgba(var(--center-channel-color-rgb), 0.08);
@ -314,13 +314,13 @@ button {
background-color: rgba(var(--error-text-color-rgb), 0.08);
color: var(--error-text);
&:hover {
&:hover, &.btn-force-hover {
background-color: rgba(var(--error-text-color-rgb), 0.12);
color: var(--error-text);
}
&:active,
&:focus {
&:active, &.btn-force-active,
&:focus, &.btn-force-focus {
background-color: rgba(var(--error-text-color-rgb), 0.16);
color: var(--error-text);
}
@ -330,11 +330,11 @@ button {
background-color: rgba(var(--sidebar-text-rgb), 0.12);
color: rgba(var(--sidebar-text-rgb), 1);
&:hover {
&:hover, &.btn-force-hover {
background-color: rgb(var(--sidebar-text-rgb), 0.16);
}
&:active,
&:active, &.btn-force-active,
&[aria-expanded="true"][aria-haspopup="true"] {
background-color: rgb(var(--sidebar-text-rgb), 0.24);
outline: none;
@ -345,22 +345,22 @@ button {
background: transparent;
color: rgb(var(--button-bg-rgb));
&:hover {
&:hover, &.btn-force-hover {
background: rgba(var(--button-bg-rgb), 0.08);
}
&:active {
&:active, &.btn-force-active {
background-color: rgb(var(--button-bg-rgb), 0.12);
}
&.btn-inverted {
color: rgb(var(--button-color-rgb));
&:hover {
&:hover, &.btn-force-hover {
background: rgba(var(--button-color-rgb), 0.12);
}
&:active,
&:active, &.btn-force-active,
&[aria-expanded="true"][aria-haspopup="true"] {
background-color: rgb(var(--button-color-rgb), 0.16);
}
@ -374,16 +374,16 @@ button {
.app__body & {
color: variables.$white;
&:hover,
&:focus,
&:active {
&:hover, &.btn-force-hover,
&:focus, &.btn-force-focus,
&:active, &.btn-force-active {
color: variables.$white;
}
}
&:hover,
&:focus,
&:active {
&:hover, &.btn-force-hover,
&:focus, &.btn-force-focus,
&:active, &.btn-force-active {
color: variables.$white;
}
}