/** * ### References: * * - https://github.com/alphagov/govuk-frontend/blob/main/src/govuk/components/button/_button.scss */ import type { StyledComponentProps } from 'styled-components'; import type { WithWhiteSpaceProps } from '@govuk-react/lib'; import styled from 'styled-components'; import * as React from 'react'; import { BORDER_WIDTH_FORM_ELEMENT, FOCUSABLE, MEDIA_QUERIES, SPACING_POINTS } from '@govuk-react/constants'; import { spacing, typography } from '@govuk-react/lib'; import { BUTTON_COLOUR, BUTTON_COLOUR_DARKEN_15, WHITE } from 'govuk-colours'; import { darken, stripUnit } from 'polished'; const BUTTON_SHADOW_SIZE = BORDER_WIDTH_FORM_ELEMENT; const RAW_SPACING_2 = SPACING_POINTS[2]; const RAW_BORDER_WIDTH = Number(stripUnit(BORDER_WIDTH_FORM_ELEMENT)); const RAW_SHADOW = Number(stripUnit(BUTTON_SHADOW_SIZE)); const HALF_SHADOW = RAW_SHADOW / 2; const BASE_PAD = RAW_SPACING_2 - RAW_BORDER_WIDTH; const StyledButton = styled('button').withConfig({ shouldForwardProp: (prop) => !['isStart', 'buttonColour', 'buttonHoverColour', 'buttonShadowColour', 'buttonTextColour', 'icon'].includes( String(prop) ), })( ({ isStart }) => typography.font({ size: isStart ? '24' : '19', lineHeight: isStart ? '1' : '19px', weight: isStart ? 'bold' : undefined, }), FOCUSABLE, ({ buttonColour = BUTTON_COLOUR, buttonHoverColour = darken(0.05, buttonColour), buttonShadowColour = buttonColour === BUTTON_COLOUR ? BUTTON_COLOUR_DARKEN_15 : darken(0.15, buttonColour), buttonTextColour = WHITE, isStart, }) => ({ boxSizing: 'border-box', display: 'inline-block', position: 'relative', width: '100%', marginTop: 0, padding: isStart // differs from govuk-frontend owing to how icons displayed ? `${BASE_PAD}px ${SPACING_POINTS[3]}px` : `${BASE_PAD - HALF_SHADOW}px ${SPACING_POINTS[2]}px`, border: `${BORDER_WIDTH_FORM_ELEMENT} solid transparent`, borderRadius: 0, color: buttonTextColour, backgroundColor: buttonColour, boxShadow: `0 ${BUTTON_SHADOW_SIZE} 0 ${buttonShadowColour}`, textAlign: 'center', verticalAlign: 'top', cursor: 'pointer', WebkitAppearance: 'none', [MEDIA_QUERIES.TABLET]: { width: 'auto', }, '&:link, &:visited, &:active, &:hover': { color: buttonTextColour, textDecoration: 'none', }, '&::-moz-focus-inner': { padding: 0, border: 0, }, '&:hover, &:focus': { backgroundColor: buttonHoverColour, }, ':active': { top: BUTTON_SHADOW_SIZE, boxShadow: 'none', }, // NB this is from govuk-frontend // Use a pseudo element to expand the click target area to include the // button's shadow as well, in case users try to click it. '::before': { content: '""', display: 'block', position: 'absolute', top: `-${BORDER_WIDTH_FORM_ELEMENT}`, right: `-${BORDER_WIDTH_FORM_ELEMENT}`, bottom: `-${RAW_BORDER_WIDTH + RAW_SHADOW}px`, left: `-${BORDER_WIDTH_FORM_ELEMENT}`, background: 'transparent', }, '&:active::before': { top: `-${RAW_BORDER_WIDTH + RAW_SHADOW}px`, }, ':disabled': { opacity: 0.5, background: buttonColour, ':hover': { backgroundColor: buttonColour, cursor: 'default', }, ':focus': { outline: 'none', }, ':active': { top: 0, boxShadow: `0 ${BUTTON_SHADOW_SIZE} 0 ${buttonShadowColour}`, }, }, ' svg': { maxWidth: '15px', [MEDIA_QUERIES.TABLET]: { marginLeft: SPACING_POINTS[2], }, }, }), // NB we drift from govuk-frontend here in how we display icons ({ icon }) => { if (icon) { return { display: 'flex', alignItems: 'center', justifyContent: 'space-between', }; } return undefined; }, spacing.withWhiteSpace({ margin: { direction: 'bottom', size: 6, adjustment: RAW_SHADOW }, }) ); const ButtonContents = styled('span')({ flexGrow: 1, }); interface StyledButtonOwnProps extends WithWhiteSpaceProps { /** * Override for default button colour */ buttonColour?: string; /** * Override for default button hover colour, * which defaults to `buttonColour` darkened by 5% */ buttonHoverColour?: string; /** * Override for default button shadow colour, * which defaults to `buttonColour` darkened by 15% */ buttonShadowColour?: string; /** * Override for default button text colour, * which defaults to govuk white */ buttonTextColour?: string; /** * Button icon */ icon?: React.ReactNode; /** * Renders a large button if set to true */ isStart?: boolean; } interface ButtonOwnProps extends React.HTMLAttributes, WithWhiteSpaceProps { /** * Button text */ children: React.ReactNode; /** * Button icon */ icon?: React.ReactNode; /** * Renders a large button if set to true */ start?: boolean; /** * Override for default button colour */ buttonColour?: string; /** * Override for default button hover colour, * which defaults to `buttonColour` darkened by 5% */ buttonHoverColour?: string; /** * Override for default button shadow colour, * which defaults to `buttonColour` darkened by 15% */ buttonShadowColour?: string; /** * Override for default button text colour, * which defaults to govuk white */ buttonTextColour?: string; } /** * Use the button component to help users carry out an action like starting an application or saving their information. * * - https://govuk-react.github.io/govuk-react/?path=/docs/button * - https://design-system.service.gov.uk/components/button/ */ export const Button: ButtonType = React.forwardRef( ({ start = false, children, icon, ...props }: ButtonOwnProps, ref: ButtonRefType) => ( {icon ? {children} : children} {icon} ) ); type ButtonRefType = React.Ref; export interface ButtonType extends React.ForwardRefExoticComponent { (props: ButtonPropsWithoutAs, ref?: ButtonRefType): React.ReactElement; // eslint-disable-next-line @typescript-eslint/no-explicit-any = 'button', FAsC extends string | React.ComponentType = AsC>( props: ButtonPropsWithAs, ref?: React.Ref ): React.ReactElement>; } type ButtonPropsWithoutAs = StyledComponentProps<'button', never, ButtonOwnProps, never> & { as?: never | undefined; forwardedAs?: never | undefined; }; type ButtonPropsWithAs< // eslint-disable-next-line @typescript-eslint/no-explicit-any AsC extends string | React.ComponentType, // eslint-disable-next-line @typescript-eslint/no-explicit-any FAsC extends string | React.ComponentType = AsC > = StyledComponentProps & { as?: AsC | undefined; forwardedAs?: FAsC | undefined; }; Button.displayName = 'Button'; export default Button;