diff --git a/src/components/button/button.module.css b/src/components/button/button.module.css new file mode 100644 index 00000000..c7c8d335 --- /dev/null +++ b/src/components/button/button.module.css @@ -0,0 +1,243 @@ +/* Button */ +.button { + composes: typo-body__primary from '@atb/theme/typography.module.css'; + cursor: pointer; + text-align: left; + border: 0; + text-decoration: none; + align-items: center; + + display: flex; + + flex-wrap: nowrap; + gap: var(--spacings-small); + + background: transparent; + color: currentColor; + + transition: all 100ms ease-in; +} +.button--display_inline { + display: inline-flex; +} +.button--disabled, +.button:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.button--state_loading { + cursor: not-allowed; +} +.button__text { + display: block; + flex: 1; +} + +.button--transparent, +.button--transparent:visited { +} +.button--transparent:hover { + color: var(--text-colors-secondary); +} +.button--transparent:active { + background-color: var(--interactive-interactive_0-active-background); + color: var(--interactive-interactive_0-active-text); +} +.button--transparent:disabled, +.button--transparent.button--disabled { + opacity: 0.6; +} +.button--transparent:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_1-outline-background); +} + +.button--transparent--underline, +.button--transparent--underline:visited { + text-decoration: underline; +} +.button--transparent--underline:hover { + color: var(--text-colors-secondary); +} +.button--transparent--underline:active { + background-color: var(--interactive-interactive_0-active-background); + color: var(--interactive-interactive_0-active-text); +} +.button--transparent--underline:disabled, +.button--transparent--underline.button--disabled { + opacity: 0.6; +} +.button--transparent--underline:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_1-outline-background); +} + +/* Interactive button 0 */ +.button--interactive_0, +.button--interactive_0:visited { + background-color: var(--interactive-interactive_0-default-background); + color: var(--interactive-interactive_0-default-text); +} +.button--interactive_0:hover { + background-color: var(--interactive-interactive_0-hover-background); + color: var(--interactive-interactive_0-hover-text); +} +.button--interactive_0:active { + background-color: var(--interactive-interactive_0-active-background); + color: var(--interactive-interactive_0-active-text); +} +.button--interactive_0:disabled, +.button--interactive_0.button--disabled { + background-color: var(--interactive-interactive_0-disabled-background); + color: var(--interactive-interactive_0-disabled-text); +} +.button--interactive_0:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_0-outline-background); +} + +/* Interactive button 1 */ + +.button--interactive_1, +.button--interactive_1:visited { + background-color: var(--interactive-interactive_1-default-background); + color: var(--interactive-interactive_1-default-text); +} +.button--interactive_1:hover { + background-color: var(--interactive-interactive_1-hover-background); + color: var(--interactive-interactive_1-hover-text); +} +.button--interactive_1:active { + background-color: var(--interactive-interactive_1-active-background); + color: var(--interactive-interactive_1-active-text); +} +.button--interactive_1:disabled, +.button--interactive_1.button--disabled { + background-color: var(--interactive-interactive_1-disabled-background); + color: var(--interactive-interactive_1-disabled-text); +} +.button--interactive_1:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_1-outline-background); +} + +/* Interactive button 2 */ + +.button--interactive_2, +.button--interactive_2:visited { + background-color: var(--interactive-interactive_2-default-background); + color: var(--interactive-interactive_2-default-text); +} +.button--interactive_2:hover { + background-color: var(--interactive-interactive_2-hover-background); + color: var(--interactive-interactive_2-hover-text); +} +.button--interactive_2:active { + background-color: var(--interactive-interactive_2-active-background); + color: var(--interactive-interactive_2-active-text); +} +.button--interactive_2:disabled, +.button--interactive_2.button--disabled { + background-color: var(--interactive-interactive_2-disabled-background); + color: var(--interactive-interactive_2-disabled-text); +} +.button--interactive_2:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_2-outline-background); +} + +/* Interactive button 3 */ + +.button--interactive_3, +.button--interactive_3:visited { + background-color: var(--interactive-interactive_3-default-background); + color: var(--interactive-interactive_3-default-text); +} +.button--interactive_3:hover { + background-color: var(--interactive-interactive_3-hover-background); + color: var(--interactive-interactive_3-hover-text); +} +.button--interactive_3:active { + background-color: var(--interactive-interactive_3-active-background); + color: var(--interactive-interactive_3-active-text); +} +.button--interactive_3:disabled, +.button--interactive_3.button--disabled { + background-color: var(--interactive-interactive_3-disabled-background); + color: var(--interactive-interactive_3-disabled-text); +} +.button--interactive_3:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_3-outline-background); +} + +/* Interactive button, destructive */ + +.button--destructive, +.button--destructive:visited { + background-color: var( + --interactive-interactive_destructive-default-background + ); + color: var(--interactive-interactive_destructive-default-text); +} +.button--destructive:hover { + background-color: var(--interactive-interactive_destructive-hover-background); + color: var(--interactive-interactive_destructive-hover-text); +} +.button--destructive:active { + background-color: var( + --interactive-interactive_destructive-active-background + ); + color: var(--interactive-interactive_destructive-active-text); +} +.button--destructive:disabled, +.button--destructive.button--disabled { + background-color: var( + --interactive-interactive_destructive-disabled-background + ); + color: var(--interactive-interactive_destructive-disabled-text); +} +.button--destructive:focus { + outline: 0; + box-shadow: inset 0 0 0 var(--border-width-medium) + var(--interactive-interactive_destructive-outline-background); +} + +.button--size_medium { + padding: var(--spacings-xLarge); +} +.button--size_small { + padding: var(--spacings-medium); +} +.button--size_compact { + padding: 0; +} + +.button--radius_top-bottom { + border-radius: var(--border-radius-regular); +} +.button--radius_top { + border-top-left-radius: var(--border-radius-regular); + border-top-right-radius: var(--border-radius-regular); +} +.button--radius_bottom { + border-bottom-left-radius: var(--border-radius-regular); + border-bottom-right-radius: var(--border-radius-regular); +} +.button--radius_top-bottom.button--radiusSize_circular { + border-radius: var(--border-radius-circle); +} +.button--radius_top.button--radiusSize_circular { + border-top-left-radius: var(--border-radius-circle); + border-top-right-radius: var(--border-radius-circle); +} +.button--radius_bottom.button--radiusSize_circular { + border-bottom-left-radius: var(--border-radius-circle); + border-bottom-right-radius: var(--border-radius-circle); +} diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx new file mode 100644 index 00000000..5c829b3f --- /dev/null +++ b/src/components/button/button.tsx @@ -0,0 +1,37 @@ +import React, {MouseEventHandler} from 'react'; +import {ButtonBase, ButtonBaseProps, getBaseButtonClassName} from './utils'; + +export type ButtonProps = { + /** Action when clicked */ + onClick?: MouseEventHandler; + /** + * Specify testID for easier access when using cypress + */ + testID?: string; + /** + * Pass properties to button element directly + */ + buttonProps?: JSX.IntrinsicElements['button']; +} & ButtonBaseProps; + +export const Button = React.forwardRef( + function Button({onClick, testID, buttonProps, ...props}, ref) { + const className = getBaseButtonClassName(props); + return ( + + ); + }, +); + +export default Button; diff --git a/src/components/button/index.ts b/src/components/button/index.ts new file mode 100644 index 00000000..103c4278 --- /dev/null +++ b/src/components/button/index.ts @@ -0,0 +1,5 @@ +export {default as ButtonLink} from './link'; +export type {ButtonLinkProps} from './link'; + +export {default as Button} from './button'; +export type {ButtonProps} from './button'; diff --git a/src/components/button/link.tsx b/src/components/button/link.tsx new file mode 100644 index 00000000..98ddc358 --- /dev/null +++ b/src/components/button/link.tsx @@ -0,0 +1,75 @@ +import Link from 'next/link'; +import React from 'react'; +import {ButtonBase, ButtonBaseProps, getBaseButtonClassName} from './utils'; + +import {UrlObject} from 'url'; + +export type ButtonLinkProps = { + /** + * Link or UrlObject passed to next/link + */ + href: string | UrlObject; + /** + * Optional additional onClick if you want to + * enrich click actions with JavaScript function + */ + onClick?: JSX.IntrinsicElements['a']['onClick']; + /** + * Specify properties to underlying a tag + */ + aProps?: JSX.IntrinsicElements['a']; + /** + * If we should do shallow routing or not. + * Shallow routing will not trigger server side props + * @default false + */ + shallow?: boolean; +} & ButtonBaseProps; + +export function ButtonLink({ + href, + onClick, + testID, + aProps = {}, + shallow = false, + ...props +}: ButtonLinkProps) { + const className = getBaseButtonClassName(props); + const extraProps = + props.state == 'active' + ? { + 'aria-current': true, + } + : {}; + + if (props.disabled || props.state == 'loading') { + return ( + + + + ); + } + + return ( + + + + + + ); +} + +export default ButtonLink; diff --git a/src/components/button/utils.tsx b/src/components/button/utils.tsx new file mode 100644 index 00000000..347f79ce --- /dev/null +++ b/src/components/button/utils.tsx @@ -0,0 +1,130 @@ +import React, { ReactNode } from 'react'; +import { and } from '@atb/utils/css'; +import style from './button.module.css'; +import { LoadingIcon } from '../loading'; + +export type ButtonModes = + | 'interactive_0' + | 'interactive_1' + | 'interactive_2' + | 'interactive_3' + | 'destructive' + | 'transparent' + | 'transparent--underline'; + +export type RadiusMode = 'top' | 'bottom' | 'top-bottom' | 'none'; + +export type ButtonBaseProps = { + /** + * Button text content + */ + title?: string; + + /** + * Button mode + * @default interactive_2 + */ + mode?: ButtonModes; + + /** + * If button is disabled or not + * @default false + */ + disabled?: boolean; + + /** + * Button padding size + * @default 'small' + */ + size?: 'medium' | 'small' | 'compact'; + + /** + * Set button state + * @default 'none' + */ + state?: 'none' | 'active' | 'loading'; + + /** + * Set where to have border radius on button + * @default 'top-bottom' + */ + radius?: RadiusMode; + + /** + * Radius of border + * @default 'regular' + */ + radiusSize?: 'regular' | 'circular'; + + /** + * Whether the button is inline or block. + * @default 'block' + */ + display?: 'block' | 'inline'; + className?: string; + testID?: string; + + /** + * Specify icon to right, left, or both. + * @see MonoIcon, + * @default undefined + */ + icon?: Partial<{ + left: ReactNode; + right: ReactNode; + }>; +}; + +export function getBaseButtonClassName({ + mode = 'interactive_2', + size = 'small', + state = 'none', + radius = 'top-bottom', + radiusSize = 'regular', + display = 'block', + disabled = false, + className = undefined, +}: ButtonBaseProps) { + const innerClassName = and( + style.button, + style[`button--${mode}`], + disabled && style[`button--disabled`], + style[`button--size_${size}`], + style[`button--state_${state}`], + style[`button--radius_${radius}`], + style[`button--radius_${radius}`], + style[`button--display_${display}`], + style[`button--radiusSize_${radiusSize}`], + className, + ); + return innerClassName; +} + +function ButtonBase({ title, icon, state }: ButtonBaseProps) { + const loadingIcon = state == 'loading' ? : null; + const hasLeftIcon = Boolean(icon?.left); + const loadingIconLeft = hasLeftIcon ? loadingIcon : null; + const loadingIconRight = !hasLeftIcon ? loadingIcon : null; + + return ( + <> + {loadingIconLeft ?? icon?.left ?? null} + {title && {title}} + {loadingIconRight ?? icon?.right ?? null} + + ); +} + +export const defaultProps: ButtonBaseProps = { + mode: 'interactive_2', + size: 'small', + state: 'none', + radius: 'top-bottom', + radiusSize: 'regular', + display: 'block', + disabled: false, +}; + +ButtonBase.defaultProps = defaultProps; + +export { ButtonBase }; diff --git a/src/components/loading/index.tsx b/src/components/loading/index.tsx new file mode 100644 index 00000000..7c67f1a9 --- /dev/null +++ b/src/components/loading/index.tsx @@ -0,0 +1,46 @@ +import { ColorIcon, ColorIconProps } from '@atb/assets/color-icon'; +import { ComponentText, useTranslation } from '@atb/translations'; +import style from './loading.module.css'; + +export type LoadingProps = { + /** + * Loading text string. + * @example Loading tickets + */ + text: string; + testID?: string; +}; +export function Loading({ text, testID }: LoadingProps) { + const { t } = useTranslation(); + + return ( +
+ +

{text}

+
+ ); +} + +export function LoadingIcon({ size }: { size?: ColorIconProps['size'] }) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/components/loading/loading.module.css b/src/components/loading/loading.module.css new file mode 100644 index 00000000..c4d0b9f6 --- /dev/null +++ b/src/components/loading/loading.module.css @@ -0,0 +1,17 @@ +.loading { + display: flex; + justify-content: center; + align-items: center; + gap: var(--spacings-medium); +} + +.loading__icon { + display: block; + animation: loading-circle 1s ease-in-out infinite; +} + +@keyframes loading-circle { + to { + transform: rotate(360deg); + } +} diff --git a/src/translations/components/index.ts b/src/translations/components/index.ts new file mode 100644 index 00000000..4b29f247 --- /dev/null +++ b/src/translations/components/index.ts @@ -0,0 +1 @@ +export { Loading } from './loading'; diff --git a/src/translations/components/loading.ts b/src/translations/components/loading.ts new file mode 100644 index 00000000..4a3e8d68 --- /dev/null +++ b/src/translations/components/loading.ts @@ -0,0 +1,5 @@ +import { translation as _ } from '@atb/translations/commons'; + +export const Loading = { + alt: _('Laster', 'Loading', 'Lastar'), +}; diff --git a/src/translations/index.ts b/src/translations/index.ts index f8cba769..b4f392ae 100644 --- a/src/translations/index.ts +++ b/src/translations/index.ts @@ -15,3 +15,4 @@ export { export * as CommonText from './common'; export * as ServerText from './server'; +export * as ComponentText from './components';