1- import React , { FC , HTMLProps } from 'react' ;
1+ import React , { EventHandler , FC , HTMLProps , KeyboardEvent , SyntheticEvent , useCallback , useRef } from 'react' ;
22import classNames from 'classnames' ;
33
4+ // Debounce timeout - default 1 second
5+ export const DefaultButtonDebounceTimeout = 1000 ;
6+
47export interface ButtonProps extends HTMLProps < HTMLButtonElement > {
58 type ?: 'button' | 'submit' | 'reset' ;
69 disabled ?: boolean ;
710 secondary ?: boolean ;
811 reverse ?: boolean ;
912 as ?: 'button' ;
13+ preventDoubleClick ?: boolean ;
14+ debounceTimeout ?: number ;
1015}
1116
1217export interface ButtonLinkProps extends HTMLProps < HTMLAnchorElement > {
1318 disabled ?: boolean ;
1419 secondary ?: boolean ;
1520 reverse ?: boolean ;
1621 as ?: 'a' ;
22+ preventDoubleClick ?: boolean ;
23+ debounceTimeout ?: number ;
24+ }
25+
26+ const useDebounceTimeout = (
27+ fn ?: EventHandler < SyntheticEvent > ,
28+ timeout : number = DefaultButtonDebounceTimeout ,
29+ ) => {
30+ const timeoutRef = useRef < number > ( ) ;
31+
32+ if ( ! fn ) return undefined ;
33+
34+ const handler : EventHandler < SyntheticEvent > = ( event ) => {
35+ event . persist ( ) ;
36+
37+ if ( timeoutRef . current ) {
38+ event . preventDefault ( ) ;
39+ event . stopPropagation ( ) ;
40+ return
41+ }
42+
43+ fn ( event ) ;
44+
45+ timeoutRef . current = window . setTimeout ( ( ) => {
46+ timeoutRef . current = undefined ;
47+ } , timeout ) ;
48+
49+ }
50+
51+ return handler ;
1752}
1853
1954export const Button : FC < ButtonProps > = ( {
@@ -22,24 +57,31 @@ export const Button: FC<ButtonProps> = ({
2257 secondary,
2358 reverse,
2459 type = 'submit' ,
60+ preventDoubleClick = false ,
61+ debounceTimeout = DefaultButtonDebounceTimeout ,
62+ onClick,
2563 ...rest
26- } ) => (
27- // eslint-disable-next-line react/button-has-type
28- < button
29- className = { classNames (
30- 'nhsuk-button' ,
31- { 'nhsuk-button--disabled' : disabled } ,
32- { 'nhsuk-button--secondary' : secondary } ,
33- { 'nhsuk-button--reverse' : reverse } ,
34- className ,
35- ) }
36- disabled = { disabled }
37- aria-disabled = { disabled ? 'true' : 'false' }
38- type = { type }
39- { ...rest }
40- />
41- ) ;
64+ } ) => {
65+ const debouncedHandleClick = useDebounceTimeout ( onClick , debounceTimeout ) ;
4266
67+ return (
68+ // eslint-disable-next-line react/button-has-type
69+ < button
70+ className = { classNames (
71+ 'nhsuk-button' ,
72+ { 'nhsuk-button--disabled' : disabled } ,
73+ { 'nhsuk-button--secondary' : secondary } ,
74+ { 'nhsuk-button--reverse' : reverse } ,
75+ className ,
76+ ) }
77+ disabled = { disabled }
78+ aria-disabled = { disabled ? 'true' : 'false' }
79+ type = { type }
80+ onClick = { preventDoubleClick ? debouncedHandleClick : onClick }
81+ { ...rest }
82+ />
83+ ) ;
84+ }
4385export const ButtonLink : FC < ButtonLinkProps > = ( {
4486 className,
4587 role = 'button' ,
@@ -48,8 +90,28 @@ export const ButtonLink: FC<ButtonLinkProps> = ({
4890 disabled,
4991 secondary,
5092 reverse,
93+ preventDoubleClick = false ,
94+ debounceTimeout = DefaultButtonDebounceTimeout ,
95+ onClick,
5196 ...rest
52- } ) => (
97+ } ) => {
98+ const debouncedHandleClick = useDebounceTimeout ( onClick , debounceTimeout ) ;
99+
100+ /**
101+ * Recreate the shim behaviour from NHS.UK/GOV.UK Frontend
102+ * https://github.com/alphagov/govuk-frontend/blob/main/packages/govuk-frontend/src/govuk/components/button/button.mjs
103+ * https://github.com/nhsuk/nhsuk-frontend/blob/main/packages/components/button/button.js
104+ */
105+ const handleKeyDown = useCallback ( ( event : KeyboardEvent < HTMLAnchorElement > ) => {
106+ const { currentTarget } = event ;
107+
108+ if ( role === 'button' && event . key === ' ' ) {
109+ event . preventDefault ( ) ;
110+ currentTarget . click ( ) ;
111+ }
112+ } , [ role ] ) ;
113+
114+ return (
53115 < a
54116 className = { classNames (
55117 'nhsuk-button' ,
@@ -61,11 +123,14 @@ export const ButtonLink: FC<ButtonLinkProps> = ({
61123 role = { role }
62124 aria-disabled = { disabled ? 'true' : 'false' }
63125 draggable = { draggable }
126+ onKeyDown = { handleKeyDown }
127+ onClick = { preventDoubleClick ? debouncedHandleClick : onClick }
64128 { ...rest }
65129 >
66130 { children }
67131 </ a >
68- ) ;
132+ ) ;
133+ }
69134
70135const ButtonWrapper : FC < ButtonLinkProps | ButtonProps > = ( { href, as, ...rest } ) => {
71136 if ( as === 'a' ) {
0 commit comments