diff --git a/libraries/core-react/jest.config.js b/libraries/core-react/jest.config.js index 7ade5bdf27..4075a7ffa4 100644 --- a/libraries/core-react/jest.config.js +++ b/libraries/core-react/jest.config.js @@ -2,7 +2,7 @@ module.exports = { verbose: true, setupFilesAfterEnv: ['./rtl.setup.ts'], transform: { - '.js': 'babel-jest', + '.(js)': 'babel-jest', '.(ts|tsx)': 'ts-jest', }, testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', diff --git a/libraries/core-react/src/Button/Button.jsx b/libraries/core-react/src/Button/Button.jsx deleted file mode 100644 index 7b84577a7b..0000000000 --- a/libraries/core-react/src/Button/Button.jsx +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-nocheck -import React, { forwardRef } from 'react' -import PropTypes from 'prop-types' -import styled, { css } from 'styled-components' -import { Icon } from '..' -import { button } from './Button.tokens' -import { typographyTemplate } from '../_common/templates' - -const { colors } = button - -// display:grid; does not work on Webkit browser engine, so we have to wrap content in element where css-grid works -const ButtonInner = styled.span` - display: grid; - grid-gap: 8px; - grid-auto-flow: column; - align-items: center; - height: 100%; -` - -const Base = ({ base, baseDisabled: disabled }) => { - const { border, spacing, typography, focus, hover } = base - - return css` - background: ${base.background}; - height: ${base.height}; - width: ${base.width}; - color: ${base.color}; - fill: ${base.color}; - svg { - justify-self: center; - height: ${button.icon_size.height}; - width: ${button.icon_size.width}; - } - - border-radius: ${border.radius}; - border-color: ${border.color}; - border-width: ${border.width}; - border-style: solid; - - ${spacing && - css` - padding-left: ${spacing.left}; - padding-right: ${spacing.right}; - `} - - ${typographyTemplate(typography)} - &::after { - position: absolute; - top: -${base.clickboundOffset}; - left: 0; - width: 100%; - height: ${base.clickbound}; - content: ''; - } - - &:hover { - background: ${hover.background}; - ${hover.radius && - css` - border-radius: ${hover.radius}; - `} - } - - &:focus { - outline: none; - } - - &[data-focus-visible-added]:focus { - outline: ${focus.width} ${focus.type} ${focus.color}; - outline-offset: 2px; - } - /* Get rid of ff focus border for buttons */ - &::-moz-focus-inner { - border: 0; - } - - &:disabled { - cursor: not-allowed; - background: ${disabled.background}; - color: ${disabled.color}; - fill: ${disabled.color}; - border-color: ${disabled.border.color}; - - &:hover { - background: ${disabled.background}; - } - } - ` -} - -const ButtonBase = styled.button` - margin: 0; - padding: 0; - ${Base} - text-decoration: none; - position: relative; - cursor: pointer; - display: inline-block; - - &::before { - position: absolute; - top: 0; - left: 0; - width: auto; - min-height: auto; - content: ''; - } -` - -export const Button = forwardRef(function Button( - { variant, children, disabled, className, color, tabIndex, href, ...other }, - ref, -) { - const colorBase = colors[color] || {} - const base = colorBase[variant] || {} - const baseDisabled = colors.disabled[variant] || {} - - const as = href ? 'a' : other.as ? other.as : 'button' - - const baseProps = { - ...other, - ref, - as, - href, - } - - return ( - - {children} - - ) -}) - -Button.propTypes = { - /** @ignore */ - children: ({ children }) => { - let error - - const iconChildIsMissingTitle = React.Children.toArray(children) - .map( - ({ type, props: childProps }) => - (type || { displayName: '' }).displayName === Icon.displayName && - !childProps.title, - ) - .includes(true) - - if (iconChildIsMissingTitle) { - error = new Error( - `When using an Icon in the Button, the property "title" is mandatory on Icon to meet accessibility requirements`, - ) - } - return error - }, - /** Specifies color */ - color: PropTypes.oneOf(['primary', 'secondary', 'danger']), - /** Specifies which variant to use */ - variant: PropTypes.oneOf(['contained', 'outlined', 'ghost', 'ghost_icon']), - /** - * If `true`, the button will be disabled. - */ - disabled: PropTypes.bool, - /** - * URL link destination - * If defined, an 'a' element is used as root instead of 'button' - */ - href: PropTypes.string, - /* User to control tabindex, default tabindex needed for button as span */ - tabIndex: PropTypes.number, - /** - * @ignore - */ - className: PropTypes.string, -} - -Button.defaultProps = { - variant: 'contained', - color: 'primary', - disabled: false, - className: '', - children: null, - href: undefined, - tabIndex: 0, -} - -Button.displayName = 'eds-button' diff --git a/libraries/core-react/src/Button/Button.test.jsx b/libraries/core-react/src/Button/Button.test.tsx similarity index 97% rename from libraries/core-react/src/Button/Button.test.jsx rename to libraries/core-react/src/Button/Button.test.tsx index a83f77ebe8..4d28e56406 100644 --- a/libraries/core-react/src/Button/Button.test.jsx +++ b/libraries/core-react/src/Button/Button.test.tsx @@ -5,7 +5,8 @@ import '@testing-library/jest-dom' import 'jest-styled-components' import styled from 'styled-components' import { save } from '@equinor/eds-icons' -import { Button, Icon } from '..' +import { Button } from './Button' +import { Icon } from '../Icon' Icon.add({ save }) diff --git a/libraries/core-react/src/Button/Button.tokens.js b/libraries/core-react/src/Button/Button.tokens.ts similarity index 55% rename from libraries/core-react/src/Button/Button.tokens.js rename to libraries/core-react/src/Button/Button.tokens.ts index 53fb65b5f8..f82cc6a2ed 100644 --- a/libraries/core-react/src/Button/Button.tokens.js +++ b/libraries/core-react/src/Button/Button.tokens.ts @@ -1,8 +1,14 @@ -// @ts-nocheck import primary from '@equinor/eds-tokens/components/button/buttons-primary.json' import secondary from '@equinor/eds-tokens/components/button/buttons-secondary.json' import danger from '@equinor/eds-tokens/components/button/buttons-danger.json' import disabled from '@equinor/eds-tokens/components/button/buttons-disabled.json' +import type { + Border, + Focus, + Hover, + Spacing, + Typography, +} from '@equinor/eds-tokens' const colors = { primary: { @@ -35,7 +41,41 @@ const colors = { }, } -export const button = { +export type Button = { + height: string + width?: string + background: string + color: string + border: Border + typography: Typography + spacing?: Partial + focus?: Focus + hover?: Hover + // TODO Remove these once figma-broker is updated with proper types + pressedColor?: string + clickboundOffset?: number | string + clickbound?: string +} + +type Buttons = { + [P in keyof typeof colors]: { + [P2 in keyof typeof colors[P]]: Button + } +} + +export type ButtonGroups = + | Buttons['primary'] + | Buttons['secondary'] + | Buttons['danger'] + | Buttons['disabled'] + +export const button: { + colors: Buttons + icon_size: { + width: string + height: string + } +} = { colors, icon_size: { width: '24px', diff --git a/libraries/core-react/src/Button/Button.tsx b/libraries/core-react/src/Button/Button.tsx new file mode 100644 index 0000000000..136e9a1d5b --- /dev/null +++ b/libraries/core-react/src/Button/Button.tsx @@ -0,0 +1,168 @@ +import React, { forwardRef, ElementType } from 'react' +import styled, { css } from 'styled-components' +import { button, Button as ButtonType, ButtonGroups } from './Button.tokens' +import { typographyTemplate } from '../_common/templates' + +const { colors } = button + +// display:grid; does not work on Webkit browser engine, so we have to wrap content in element where css-grid works +const ButtonInner = styled.span` + display: grid; + grid-gap: 8px; + grid-auto-flow: column; + align-items: center; + height: 100%; +` + +const Base = ({ + token, + disabledToken, +}: { + token: ButtonType + disabledToken: ButtonType +}) => { + const { border, spacing, typography, focus, hover } = token + + return css` + background: ${token.background}; + height: ${token.height}; + width: ${token.width}; + color: ${token.color}; + fill: ${token.color}; + svg { + justify-self: center; + height: ${button.icon_size.height}; + width: ${button.icon_size.width}; + } + + border-radius: ${border.radius}; + border-color: ${border.color}; + border-width: ${border.width}; + border-style: solid; + + ${spacing && + css` + padding-left: ${spacing.left}; + padding-right: ${spacing.right}; + `} + + ${typographyTemplate(typography)} + &::after { + position: absolute; + top: -${token.clickboundOffset}; + left: 0; + width: 100%; + height: ${token.clickbound}; + content: ''; + } + + &:hover { + background: ${hover.background}; + ${hover.radius && + css` + border-radius: ${hover.radius}; + `} + } + + &:focus { + outline: none; + } + + &[data-focus-visible-added]:focus { + outline: ${focus.width} ${focus.type} ${focus.color}; + outline-offset: 2px; + } + /* Get rid of ff focus border for buttons */ + &::-moz-focus-inner { + border: 0; + } + + &:disabled { + cursor: not-allowed; + background: ${disabledToken.background}; + color: ${disabledToken.color}; + fill: ${disabledToken.color}; + border-color: ${disabledToken.border.color}; + + &:hover { + background: ${disabledToken.background}; + } + } + ` +} + +const ButtonBase = styled.button` + margin: 0; + padding: 0; + ${Base} + text-decoration: none; + position: relative; + cursor: pointer; + display: inline-block; + + &::before { + position: absolute; + top: 0; + left: 0; + width: auto; + min-height: auto; + content: ''; + } +` +type Props = { + /** Specifies color */ + color?: 'primary' | 'secondary' | 'danger' + /** Specifies which variant to use */ + variant?: 'contained' | 'outlined' | 'ghost' | 'ghost_icon' + /** + * URL link destination + * If defined, an 'a' element is used as root instead of 'button' + */ + href?: string + /** Is disabled */ + disabled?: boolean + /** Change html element */ + as?: ElementType + /** ttype */ + type?: string +} & React.HTMLAttributes + +export const Button = forwardRef(function Button( + { + color = 'danger', + variant = 'contained', + children, + disabled, + href, + ...other + }, + ref, +) { + const colorBase: ButtonGroups | Partial = colors[color] || {} + const token = colorBase[variant] || {} + const disabledToken = colors.disabled[variant] || {} + + const as: ElementType = href ? 'a' : other.as ? other.as : 'button' + const type = href || other.as ? undefined : 'button' + const tabIndex = disabled ? -1 : other.tabIndex + + const buttonProps = { + ref, + as, + href, + type, + token, + disabledToken, + disabled, + tabIndex, + ...other, + } + + return ( + + {children} + + ) +}) + +Button.displayName = 'EdsButton' diff --git a/libraries/core-react/src/Button/index.js b/libraries/core-react/src/Button/index.ts similarity index 69% rename from libraries/core-react/src/Button/index.js rename to libraries/core-react/src/Button/index.ts index 92d23cb3e8..4d0a670f4a 100644 --- a/libraries/core-react/src/Button/index.js +++ b/libraries/core-react/src/Button/index.ts @@ -1,2 +1 @@ -// @ts-nocheck export { Button } from './Button' diff --git a/libraries/tokens/src/types.ts b/libraries/tokens/src/types.ts index 6aba877d0b..85ce604ebe 100644 --- a/libraries/tokens/src/types.ts +++ b/libraries/tokens/src/types.ts @@ -22,9 +22,10 @@ export type TypographyTokens = { } export type Border = { + type?: 'outline' | 'border' radius: string color?: string - width?: string + width?: string | number } export type Spacing = { @@ -63,3 +64,28 @@ export type Spacings = { xx_small: string } } + +export type Focus = { + type?: 'dashed' | string + color: string + width: string + gap?: string +} + +export type Hover = { + background: string + radius?: string +} + +export type Pressed = { + color: string +} + +export type Clickbound = { + height: string + width: string + offset: { + x: string + y: string + } +}