From c19814f6b3b983687bf76d15aa7488fab1165b23 Mon Sep 17 00:00:00 2001 From: Birk Johansson Date: Tue, 20 Feb 2024 16:54:17 +0100 Subject: [PATCH] fix: add linkbutton component --- .../LinkButton/LinkButton.module.css | 288 ++++++++++++++++++ src/components/LinkButton/LinkButton.tsx | 87 ++++++ src/components/LinkButton/index.ts | 1 + .../listActions/SectionListActions.module.css | 3 +- .../listActions/SectionListActions.tsx | 19 +- 5 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 src/components/LinkButton/LinkButton.module.css create mode 100644 src/components/LinkButton/LinkButton.tsx create mode 100644 src/components/LinkButton/index.ts diff --git a/src/components/LinkButton/LinkButton.module.css b/src/components/LinkButton/LinkButton.module.css new file mode 100644 index 00000000..9635fea8 --- /dev/null +++ b/src/components/LinkButton/LinkButton.module.css @@ -0,0 +1,288 @@ +.linkButton { + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 4px; + font-weight: 400; + letter-spacing: 0.5px; + text-decoration: none; + cursor: pointer; + user-select: none; + color: var(--colors-grey900); + + /*medium*/ + height: 36px; + padding: 0 var(--spacers-dp12); + font-size: 14px; + line-height: 16px; + + /*basic*/ + border: 1px solid var(--colors-grey500); + background-color: #f9fafb; +} + +.linkButton:disabled { + cursor: not-allowed; +} + +.linkButton:focus { + outline: 3px solid blue; + outline-offset: -3px; + text-decoration: underline; +} + +/* Prevent focus styles when mouse clicking */ +.linkButton:focus:not(:focus-visible) { + outline: none; + text-decoration: none; +} + +/* Prevent focus styles on active and disabled buttons */ +.linkButton:active:focus, +.linkButton:disabled:focus { + outline: none; + text-decoration: none; +} + +.linkButton:hover { + border-color: var(--colors-grey500); + background-color: var(--colors-grey200); +} + +.linkButton:active, +.linkButton:active:focus { + border-color: var(--colors-grey500); + background-color: var(--colors-grey200); + box-shadow: 0 0 0 1px rgb(0, 0, 0, 0.1) inset; +} + +.linkButton:focus { + background-color: #f9fafb; +} + +.linkButton:disabled { + border-color: var(--colors-grey400); + background-color: #f9fafb; + box-shadow: none; + color: var(--theme-disabled); + fill: var(--theme-disabled); +} + +.small { + height: 28px; + padding: 0 6px; + font-size: 14px; + line-height: 16px; +} + +.large { + height: 43px; + padding: 0 var(--spacers-dp24); + font-size: 16px; + letter-spacing: 0.57px; + line-height: 19px; +} + +.primary { + border-color: var(--theme-primary800); + background: linear-gradient(180deg, #1565c0 0%, #0650a3 100%); + background-color: #2b61b3; + color: var(--colors-white); + fill: var(--colors-white); + font-weight: 500; +} + +.primary:hover { + border-color: var(--theme-primary800); + background: linear-gradient(180deg, #054fa3 0%, #034793 100%); + background-color: #21539f; +} + +.primary:active, +.primary:active:focus { + background: linear-gradient(180deg, #054fa3 0%, #034793 100%); + background-color: #1c4a90; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18) inset; +} + +.primary:focus { + background: var(--colors-blue800); + border-color: var(--colors-blue900); + outline-offset: -5px; +} + +.primary:disabled { + border-color: #93a6bd; + background: #b3c6de; + box-shadow: none; + color: var(--colors-white); + fill: var(--colors-white); +} + +.secondary { + border-color: rgba(74, 87, 104, 0.25); + background-color: transparent; +} + +.secondary:hover { + border-color: rgba(74, 87, 104, 0.5); + background-color: rgba(160, 173, 186, 0.05); +} + +.secondary:active, +.secondary:active:focus { + background-color: rgba(160, 173, 186, 0.2); + box-shadow: none; +} + +.secondary:focus { + background-color: transparent; +} + +.secondary:disabled { + border-color: rgba(74, 87, 104, 0.25); + background-color: transparent; + box-shadow: none; + color: var(--theme-disabled); + fill: var(--theme-disabled); +} + +.destructive { + border-color: #a10b0b; + background: linear-gradient(180deg, #d32f2f 0%, #b71c1c 100%); + background-color: #b9242b; + color: var(--colors-white); + fill: var(--colors-white); + font-weight: 500; +} + +.destructive:hover { + border-color: #a10b0b; + background: linear-gradient(180deg, #b81c1c 0%, #b80c0b 100%); + background-color: #ac0f1a; +} + +.destructive:active, +.destructive:active:focus { + background: linear-gradient(180deg, #b81c1c 0%, #b80c0b 100%); + background-color: #ac101b; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18) inset; +} + +.destructive:focus { + background: linear-gradient(180deg, #d32f2f 0%, #b71c1c 100%); + background-color: #b72229; +} + +.destructive:disabled { + border-color: #c59898; + background: #d6a8a8; + box-shadow: none; + color: var(--colors-white); + fill: var(--colors-white); +} + +.destructive.secondary { + border-color: rgba(74, 87, 104, 0.25); + background: transparent; + color: var(--colors-red700); + fill: var(--colors-red700); + font-weight: 400; +} + +.destructive.secondary:hover { + border-color: var(--colors-red600); + background: var(--colors-red050); + color: var(--colors-red800); + fill: var(--colors-red800); +} + +.destructive.secondary:active, +.destructive.secondary:active:focus { + background: var(--colors-red100); + border-color: var(--colors-red700); + box-shadow: none; +} + +.destructive.secondary:disabled { + background: transparent; + border-color: rgba(74, 87, 104, 0.25); + color: rgba(183, 28, 28, 0.6); + fill: rgba(183, 28, 28, 0.6); +} + +.icon-only { + padding: 0 0 0 5px; +} + +.button-icon { + margin-right: 6px; + color: inherit; + fill: inherit; + font-size: 26px; + vertical-align: middle; + pointer-events: none; +} + +.icon-only .button-icon { + margin-right: 5px; +} + +.small.icon-only { + padding: 0 4px 0 5px; +} + +.small .button-icon { + margin-right: 2px; +} + +.small.icon-only .button-icon { + margin-right: 1px; +} + +.toggled { + background: var(--colors-grey700); + border: 1px solid var(--colors-grey900); + color: var(--colors-grey050); + fill: var(--colors-grey050); +} + +.toggled:focus { + background: var(--colors-grey800); +} + +.toggled:hover { + background: var(--colors-grey800); + border-color: var(--colors-grey900); +} + +.toggled:active, +.toggled:active:focus { + background: var(--colors-grey900); + border-color: var(--colors-grey900); +} + +.toggled:disabled { + background: var(--colors-grey500); + border-color: var(--colors-grey600); + color: var(--colors-grey050); + fill: var(--colors-grey050); +} + +.loader { + width: 16px; + height: 16px; + margin-right: 8px; +} + +.loader + .button-icon { + display: none; +} + +.icon-only .loader { + margin: 0 8px 0 4px; +} +.small.icon-only .loader { + margin: 0 4px; +} diff --git a/src/components/LinkButton/LinkButton.tsx b/src/components/LinkButton/LinkButton.tsx new file mode 100644 index 00000000..528ac879 --- /dev/null +++ b/src/components/LinkButton/LinkButton.tsx @@ -0,0 +1,87 @@ +import { ButtonProps } from '@dhis2/ui' +import cx from 'classnames' +import React, { AnchorHTMLAttributes } from 'react' +import { useLinkClickHandler, useHref } from 'react-router-dom' +import css from './LinkButton.module.css' + +type UseLinkClickHandlerParameters = Parameters + +type LinkClickHandlerOptions = UseLinkClickHandlerParameters[1] + +type RelevantButtonProps = Pick< + ButtonProps, + | 'disabled' + | 'className' + | 'primary' + | 'secondary' + | 'small' + | 'toggled' + | 'large' + | 'destructive' +> + +type LinkButtonProps = AnchorHTMLAttributes & + LinkClickHandlerOptions & + RelevantButtonProps & { + to: Parameters[0] + } + +/* Wrapping button with anchor-tags are not valid, style anchor as a UI-button */ +export const LinkButton = ({ + onClick, + disabled, + className, + primary, + secondary, + small, + toggled, + large, + destructive, + target, + replace, + state, + preventScrollReset, + relative, + to, + href, + ...anchorProps +}: LinkButtonProps) => { + const resolvedHref = useHref(to, { relative }) + const handleClickInternal = useLinkClickHandler(to, { + replace, + state, + preventScrollReset, + relative, + target, + }) + + const handleClick = ( + event: React.MouseEvent + ) => { + if (onClick) { + onClick(event) + } + if (!event.defaultPrevented) { + handleClickInternal(event) + } + } + + const resolvedClassname = cx(css.linkButton, className, { + [css.disabled]: disabled, + [css.primary]: primary, + [css.secondary]: secondary, + [css.destructive]: destructive, + [css.toggled]: toggled, + [css.large]: large, + [css.small]: small, + }) + return ( + + ) +} diff --git a/src/components/LinkButton/index.ts b/src/components/LinkButton/index.ts new file mode 100644 index 00000000..793960cd --- /dev/null +++ b/src/components/LinkButton/index.ts @@ -0,0 +1 @@ +export * from './LinkButton' diff --git a/src/components/sectionList/listActions/SectionListActions.module.css b/src/components/sectionList/listActions/SectionListActions.module.css index dd09368d..8e42de04 100644 --- a/src/components/sectionList/listActions/SectionListActions.module.css +++ b/src/components/sectionList/listActions/SectionListActions.module.css @@ -3,7 +3,8 @@ gap: var(--spacers-dp8); } -.listActions button { +.listActions button, +.listActions a { padding: 0 2px !important; } diff --git a/src/components/sectionList/listActions/SectionListActions.tsx b/src/components/sectionList/listActions/SectionListActions.tsx index 6689bee8..8c71ec99 100644 --- a/src/components/sectionList/listActions/SectionListActions.tsx +++ b/src/components/sectionList/listActions/SectionListActions.tsx @@ -11,6 +11,7 @@ import { } from '@dhis2/ui' import React, { useRef, useState } from 'react' import { Link, useHref, useLinkClickHandler } from 'react-router-dom' +import { LinkButton } from '../../LinkButton' import css from './SectionListActions.module.css' export const ListActions = ({ children }: React.PropsWithChildren) => { @@ -19,11 +20,9 @@ export const ListActions = ({ children }: React.PropsWithChildren) => { export const ActionEdit = ({ modelId }: { modelId: string }) => { return ( - - - + + + ) } @@ -38,7 +37,8 @@ export const ActionMore = ({ const [open, setOpen] = useState(false) const ref = useRef(null) const href = useHref(modelId, { relative: 'path' }) - const handleClick = useLinkClickHandler(modelId) + + const handleEditClick = useLinkClickHandler(modelId) return (
@@ -61,14 +61,17 @@ export const ActionMore = ({ dense label={i18n.t('Show details')} icon={} - onClick={onShowDetailsClick} + onClick={() => { + onShowDetailsClick() + setOpen(false) + }} /> } onClick={(_, e) => { - handleClick(e) + handleEditClick(e) setOpen(false) }} target="_blank"