Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 9a91b54

Browse files
tripleWdotcomSimonRichter
authored andcommitted
feat(button): added a new button with icon and text
The IconTextButton is a mix between Button and IconButton and will always have a 'primary' color and a 'secondary' color text/title. Co-authored-by: Simon Richter <simon.richter@axis.com>
1 parent dc300b0 commit 9a91b54

File tree

9 files changed

+758
-102
lines changed

9 files changed

+758
-102
lines changed

packages/core/src/Button/__snapshots__/index.test.tsx.snap

Lines changed: 477 additions & 39 deletions
Large diffs are not rendered by default.

packages/core/src/Button/index.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react'
22
import 'jest-styled-components'
33

4-
import { Button, IconButton } from '.'
5-
import { AddIcon } from 'practical-react-components-icons'
4+
import { Button, IconButton, IconTextButton } from '.'
5+
import { AddIcon, CloseIcon } from 'practical-react-components-icons'
66
import { TestRender } from '../TestUtils'
77

88
const onClick = () => {
@@ -135,3 +135,22 @@ describe('IconButton', () => {
135135
expect(tree2).toMatchSnapshot()
136136
})
137137
})
138+
139+
describe('IconTextButton', () => {
140+
test('primary', () => {
141+
const tree1 = TestRender(
142+
<IconTextButton label="Click me" icon={CloseIcon} onClick={onClick} />
143+
)
144+
expect(tree1).toMatchSnapshot()
145+
146+
const tree2 = TestRender(
147+
<IconTextButton
148+
label="Click me"
149+
icon={CloseIcon}
150+
onClick={onClick}
151+
disabled={true}
152+
/>
153+
)
154+
expect(tree2).toMatchSnapshot()
155+
})
156+
})

packages/core/src/Button/index.tsx

Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
import React, { useCallback } from 'react'
22
import styled, { css } from 'styled-components'
33
import { useVisibleFocus } from 'react-hooks-shareable'
4-
54
import { Icon, IconType } from '../Icon'
65
import { Typography } from '../Typography'
76
import { componentSize, opacity, spacing, shape } from '../designparams'
87

98
// Button min-width should be "Cancel" button width
109
const BUTTON_MIN_WIDTH = '83px'
1110

11+
//Common CSS for NativeButton and NativeIconTextButton
12+
const COMMON_STYLE = css`
13+
white-space: nowrap;
14+
height: ${componentSize.small};
15+
outline: none;
16+
&::-moz-focus-inner {
17+
border: 0;
18+
}
19+
border-radius: ${shape.radius.small};
20+
cursor: pointer;
21+
user-select: none;
22+
transition: all 200ms;
23+
max-width: 100%;
24+
`
25+
1226
/**
1327
* Button
1428
*
@@ -24,20 +38,9 @@ export const NativeButton = styled.button<{
2438
readonly icon?: IconType
2539
readonly visibleFocus: boolean
2640
}>`
27-
white-space: nowrap;
28-
max-width: 100%;
41+
${COMMON_STYLE}
2942
min-width: ${BUTTON_MIN_WIDTH};
30-
height: ${componentSize.small};
31-
outline: none;
32-
&::-moz-focus-inner {
33-
border: 0;
34-
}
3543
border: 2px solid transparent;
36-
border-radius: ${shape.radius.small};
37-
cursor: pointer;
38-
user-select: none;
39-
transition: all 200ms;
40-
4144
padding: ${({ icon }) =>
4245
icon === undefined
4346
? `0 ${spacing.large}`
@@ -497,3 +500,150 @@ export const IconButton = React.forwardRef<BaseElement, IconButtonProps>(
497500
)
498501
}
499502
)
503+
504+
/**
505+
* IconTextButton
506+
*
507+
* Always has a primary icon and a secondary text
508+
*
509+
*/
510+
511+
export const NativeIconTextButton = styled.button<{
512+
readonly visibleFocus: boolean
513+
}>`
514+
${COMMON_STYLE}
515+
border: none;
516+
padding: 0 ${spacing.large} 0 0;
517+
${({ visibleFocus, theme }) => {
518+
return css`
519+
color: ${theme.color.text04()};
520+
fill: ${theme.color.text04()};
521+
background-color: transparent;
522+
523+
&:hover {
524+
color: ${theme.color.text03()};
525+
background-color: ${theme.color.element11(opacity[16])};
526+
${IconContainer} {
527+
background-color: ${theme.color.textPrimary()};
528+
}
529+
}
530+
&:focus {
531+
${visibleFocus
532+
? css`
533+
color: ${theme.color.text04()};
534+
background-color: ${theme.color.element11(opacity[16])};
535+
`
536+
: undefined};
537+
}
538+
&:active {
539+
box-shadow: 0 0 0 4px ${theme.color.elementPrimary(opacity[24])};
540+
background-color: ${theme.color.element11(opacity[24])};
541+
}
542+
&:disabled {
543+
opacity: ${opacity[48]};
544+
cursor: default;
545+
box-shadow: none;
546+
&:hover {
547+
${IconContainer} {
548+
background-color: ${theme.color.elementPrimary()};
549+
}
550+
}
551+
}
552+
`
553+
}}
554+
`
555+
const IconContainer = styled(Icon)`
556+
${({ theme }) => {
557+
return css`
558+
height: ${componentSize.small};
559+
width: ${componentSize.small};
560+
color: ${theme.color.text00()};
561+
background-color: ${theme.color.elementPrimary()};
562+
border-radius: ${shape.radius.small};
563+
margin-right: ${spacing.medium};
564+
padding: ${spacing.small};
565+
transition: all 200ms;
566+
`
567+
}}
568+
`
569+
export interface IconTextButtonProps
570+
extends Omit<BaseButtonProps, 'variant' | 'accent'> {
571+
/**
572+
* String used to label the button.
573+
*/
574+
readonly label: string
575+
/**
576+
* The icon element.
577+
*/
578+
readonly icon: IconType
579+
}
580+
581+
// eslint-disable-next-line react/display-name
582+
export const IconTextButton = React.forwardRef<
583+
BaseElement,
584+
IconTextButtonProps
585+
>(
586+
(
587+
{
588+
disabled = false,
589+
type = 'button',
590+
icon,
591+
onPointerDown,
592+
onPointerUp,
593+
onFocus,
594+
label,
595+
...props
596+
},
597+
ref
598+
) => {
599+
const {
600+
isPointerOn,
601+
isPointerOff,
602+
determineVisibleFocus,
603+
visibleFocus,
604+
} = useVisibleFocus()
605+
606+
const handleFocus = useCallback<React.FocusEventHandler<BaseElement>>(
607+
e => {
608+
onFocus?.(e)
609+
determineVisibleFocus()
610+
},
611+
[determineVisibleFocus, onFocus]
612+
)
613+
const handlePointerDown = useCallback<
614+
React.PointerEventHandler<BaseElement>
615+
>(
616+
e => {
617+
onPointerDown?.(e)
618+
isPointerOn()
619+
},
620+
[isPointerOn, onPointerDown]
621+
)
622+
const handlePointerUp = useCallback<React.PointerEventHandler<BaseElement>>(
623+
e => {
624+
onPointerUp?.(e)
625+
isPointerOff()
626+
},
627+
[isPointerOff, onPointerUp]
628+
)
629+
return (
630+
<NativeIconTextButton
631+
ref={ref}
632+
disabled={disabled}
633+
type={type}
634+
onPointerDown={handlePointerDown}
635+
onPointerUp={handlePointerUp}
636+
onFocus={handleFocus}
637+
{...props}
638+
visibleFocus={visibleFocus}
639+
>
640+
<Container>
641+
<IconContainer icon={icon} />
642+
<LabelContainer variant="primary" accent={false}>
643+
<Typography variant="button-text">{label}</Typography>
644+
</LabelContainer>
645+
</Container>
646+
</NativeIconTextButton>
647+
)
648+
}
649+
)

0 commit comments

Comments
 (0)