Commit 41105c91 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Modernize styling of `AdderToolbar`

Convert the `AdderToolbar` component to tailwind utility-first styles.

This component has some styling complexity that requires the continued
use of some external styles for now:

* It serves as the root element in a shadow DOM and has a couple of
  reset rules not serviceable by tailwind utility classes
* The hover behavior of the adder toolbar buttons is too complex to
  implement with utility styles alone.

Visual differences before/after are negligible. The adder is 1px taller
due to using standardized spacing.
parent 429d0ea6
...@@ -3,6 +3,58 @@ import { LabeledButton, Icon } from '@hypothesis/frontend-shared'; ...@@ -3,6 +3,58 @@ import { LabeledButton, Icon } from '@hypothesis/frontend-shared';
import { useShortcut } from '../../shared/shortcut'; import { useShortcut } from '../../shared/shortcut';
/**
* Render an inverted light-on-dark "pill" with the given `badgeCount`
* (annotation count). This is rendered instead of an icon on the toolbar
* button for "show"-ing associated annotations for the current selection.
*
* @param {object} props
* @param {number} props.badgeCount
*/
function NumberIcon({ badgeCount }) {
return (
<span
className={classnames(
'rounded px-1 py-0.5',
'text-color-text-inverted font-bold bg-grey-7',
'dim-bg'
)}
>
{badgeCount}
</span>
);
}
/**
* Render an arrow pointing up or down from the AdderToolbar. This arrow
* should point roughly to the end of the user selection in the document.
*
* @param {object} props
* @param {'up'|'down'} props.arrowDirection
*/
function AdderToolbarArrow({ arrowDirection }) {
return (
<Icon
name="pointer"
classes={classnames(
// Position the arrow in the horizontal center at the bottom of the
// container (toolbar). Note: the arrow is pointing up at this point.
'absolute left-1/2 -translate-x-1/2',
// Override `1em` width/height rules in `Icon` to size the arrow as
// its SVG dimensions dictate
'h-auto w-auto z-10',
'text-grey-3 fill-white',
{
// Down arrow: transform to point the arrow down
'rotate-180': arrowDirection === 'down',
// Up arrow: position vertically above the toolbar
'top-0 -translate-y-full': arrowDirection === 'up',
}
)}
/>
);
}
/** /**
* @param {object} props * @param {object} props
* @param {number} [props.badgeCount] * @param {number} [props.badgeCount]
...@@ -18,17 +70,18 @@ function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) { ...@@ -18,17 +70,18 @@ function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) {
return ( return (
<LabeledButton <LabeledButton
classes="LabeledIconButton AdderToolbar__button" className={classnames(
'flex flex-col gap-y-1 items-center py-2.5 px-2',
'text-annotator-sm leading-none text-grey-7',
'transition-colors duration-200',
'dim-item'
)}
icon={icon} icon={icon}
onClick={onClick} onClick={onClick}
title={title} title={title}
> >
{typeof badgeCount === 'number' && ( {typeof badgeCount === 'number' && <NumberIcon badgeCount={badgeCount} />}
<span className="hyp-u-bg-color--grey-7 AdderToolbar__badge"> <span className="font-normal">{label}</span>
{badgeCount}
</span>
)}
<span className="LabeledIconButton__label">{label}</span>
</LabeledButton> </LabeledButton>
); );
} }
...@@ -54,8 +107,8 @@ function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) { ...@@ -54,8 +107,8 @@ function ToolbarButton({ badgeCount, icon, label, onClick, shortcut }) {
*/ */
/** /**
* The toolbar that is displayed above selected text in the document providing * The toolbar that is displayed above or below selected text in the document,
* options to create annotations or highlights. * providing options to create annotations or highlights.
* *
* @param {AdderToolbarProps} props * @param {AdderToolbarProps} props
*/ */
...@@ -82,17 +135,25 @@ export default function AdderToolbar({ ...@@ -82,17 +135,25 @@ export default function AdderToolbar({
return ( return (
<div <div
className={classnames( className={classnames(
'hyp-u-border hyp-u-bg-color--white',
'AdderToolbar', 'AdderToolbar',
'absolute select-none bg-white rounded shadow-adderToolbar',
// Because `.AdderToolbar` rules reset `all:initial`, we cannot use
// default border values from Tailwind and have to be explicit about
// all border attributes
'border border-solid border-grey-3',
// Start at a very low opacity as we're going to fade in in the animation
'opacity-5',
{ {
'AdderToolbar--down': arrowDirection === 'up', 'animate-adderPopUp': arrowDirection === 'up' && isVisible,
'AdderToolbar--up': arrowDirection === 'down', 'animate-adderPopDown': arrowDirection === 'down' && isVisible,
'is-active': isVisible,
} }
)} )}
style={{ visibility: isVisible ? 'visible' : 'hidden' }} dir="ltr"
style={{
visibility: isVisible ? 'visible' : 'hidden',
}}
> >
<div className="hyp-u-layout-row AdderToolbar__actions"> <div className="flex dim-items-on-hover">
<ToolbarButton <ToolbarButton
icon="annotate" icon="annotate"
onClick={() => onCommand('annotate')} onClick={() => onCommand('annotate')}
...@@ -106,24 +167,23 @@ export default function AdderToolbar({ ...@@ -106,24 +167,23 @@ export default function AdderToolbar({
shortcut={highlightShortcut} shortcut={highlightShortcut}
/> />
{annotationCount > 0 && ( {annotationCount > 0 && (
<div className="hyp-u-margin--2 AdderToolbar__separator" /> <>
)} <div
{annotationCount > 0 && ( className={classnames(
<ToolbarButton // Style a vertical separator line
badgeCount={annotationCount} 'm-1.5 border-r border-grey-4 border-solid'
onClick={() => onCommand('show')} )}
label="Show" />
shortcut={showShortcut} <ToolbarButton
/> badgeCount={annotationCount}
onClick={() => onCommand('show')}
label="Show"
shortcut={showShortcut}
/>
</>
)} )}
</div> </div>
<Icon <AdderToolbarArrow arrowDirection={arrowDirection} />
name="pointer"
classes={classnames('AdderToolbar__arrow', {
'AdderToolbar__arrow--down': arrowDirection === 'down',
'AdderToolbar__arrow--up': arrowDirection === 'up',
})}
/>
</div> </div>
); );
} }
@use '../variables' as var; @layer components {
// Main class for the root element of the "adder" toolbar that appears when the
$adder-transition-duration: 80ms; // user makes a text selection.
.AdderToolbar {
// Main class for the root element of the "adder" toolbar that appears when the // Reset all inherited properties to their initial values. This prevents CSS
// user makes a text selection. // property values from the host page being inherited by elements of the
.AdderToolbar { // Adder, even when using Shadow DOM.
// Reset all inherited properties to their initial values. This prevents CSS all: initial;
// property values from the host page being inherited by elements of the
// Adder, even when using Shadow DOM. // Provide some hover-related behavior that cannot be feasibly duplicated
all: initial; // using utility styles.
//
position: absolute; // When the container/parent element containing the annotator toolbar
box-sizing: border-box; // buttons is hovered, dim the buttons except the specifically-hovered
direction: ltr; // button, which should be darkened. The "annotate" and "highlight" buttons
border-radius: var.$annotator-border-radius; // use icons and a label, so dimming/darkening their text color suffices,
// but the badge-count pill needs its background-color dimmed/darkened as
// Prevent the browser from selecting text in the adder itself during text // well (`.dim-bg`).
// selection. .dim-items-on-hover:hover {
// .dim-item:not(:hover) {
// See https://github.com/hypothesis/product-backlog/issues/878 @apply text-grey-5;
user-select: none;
.dim-bg {
// The adder toolbar has a distinctive, broad-spreading drop shadow @apply bg-grey-5;
box-shadow: 0px 2px 10px 0px rgba(0, 0, 0, 0.25); }
}
// Give the adder a very low opacity initially. It will then fade-in when
// shown. .dim-item:hover {
opacity: 0.05; @apply text-grey-9;
// Adder entry animation settings .dim-bg {
animation-duration: $adder-transition-duration; @apply bg-grey-9;
animation-timing-function: ease-in; }
animation-fill-mode: forwards; }
&--down.is-active {
animation-name: adder-fade-in, adder-pop-down;
}
&--up.is-active {
animation-name: adder-fade-in, adder-pop-up;
}
}
.AdderToolbar__arrow {
position: absolute;
z-index: 2;
color: var.$color-border;
fill: var.$color-background;
// Horizontal centering, part 1
left: 50%;
// Override `Icon` default 1em sizing
height: auto;
width: auto;
&--down {
// These transforms:
// 1) Translate -50% (left) on the X axis to complete horizontal centering
// 2) Translate up (Y) by 1px to eliminate a faint border line visible in Safari
transform: rotateX(180deg) translateX(-50%) translateY(1px);
}
&--up {
// Align at top edge of parent element...
top: 0;
// Translate on X axis -50% (left) to complete horizontal centering
// Translate -100% on Y axis (up) to position arrow above parent
transform: translate(-50%, -100%);
}
}
.AdderToolbar__badge {
// The badge should be vertically aligned with icons in other toolbar buttons
// and the label underneath should also align with labels in other buttons.
border-radius: var.$annotator-border-radius;
color: var.$color-text--inverted;
font-weight: bold;
// For positioning the number appropriately within the badge background
padding: 2px 4px;
}
.AdderToolbar__separator {
// Override border color to be a little darker
border-right: 1px solid var.$grey-4;
}
// The toolbar has full contrast when not hovered. When the toolbar is hovered,
// the buttons are dimmed except for the one which is currently hovered.
.AdderToolbar__actions:hover {
.AdderToolbar__button:not(:hover) {
color: var.$grey-semi;
.AdderToolbar__badge {
background-color: var.$grey-semi;
} }
} }
} }
@keyframes adder-fade-in {
0% {
opacity: 0.05;
}
20% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
@keyframes adder-pop-up {
from {
transform: scale(0.8) translateY(10px);
}
to {
transform: scale(1) translateY(0px);
}
}
@keyframes adder-pop-down {
from {
transform: scale(0.8) translateY(-10px);
}
to {
transform: scale(1) translateY(0px);
}
}
...@@ -33,32 +33,6 @@ ...@@ -33,32 +33,6 @@
} }
} }
// Used in the AdderToolbar, a transparent button that has an icon and a small
// label below the icon
.LabeledIconButton {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
&:hover:not([disabled]) {
background-color: transparent;
}
cursor: pointer;
font-weight: normal;
font-size: var.$annotator-adder-font-size;
line-height: var.$annotator-adder-line-height;
padding: var.$layout-space--small;
transition: color 80ms;
background: transparent;
&__label {
margin-top: var.$layout-space--xxsmall;
transition: color 80ms;
}
}
// Override any background color on a LabeledButton // Override any background color on a LabeledButton
.TransparentButton { .TransparentButton {
background-color: transparent; background-color: transparent;
......
...@@ -123,7 +123,6 @@ $sidebar--theme-clean: '.theme-clean'; ...@@ -123,7 +123,6 @@ $sidebar--theme-clean: '.theme-clean';
// ----------------------------------- // -----------------------------------
$annotator-toolbar-width: 33px; $annotator-toolbar-width: 33px;
$annotator-base-font-size: 14px;
$annotator-base-line-height: 20px; $annotator-base-line-height: 20px;
$annotator-adder-font-size: 12px; $annotator-adder-font-size: 12px;
......
...@@ -10,11 +10,21 @@ export default { ...@@ -10,11 +10,21 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
animation: {
adderPopUp: 'adderPopUp 0.08s ease-in forwards',
adderPopDown: 'adderPopDown 0.08s ease-in forwards',
},
boxShadow: {
adderToolbar: '0px 2px 10px 0px rgba(0, 0, 0, 0.25)',
},
colors: { colors: {
slate: { slate: {
// TODO remove when available from upstream preset // TODO remove when available from upstream preset
1: '#e3e3e5', 1: '#e3e3e5',
}, },
'color-text': {
inverted: '#f2f2f2',
},
}, },
fontFamily: { fontFamily: {
sans: [ sans: [
...@@ -31,9 +41,41 @@ export default { ...@@ -31,9 +41,41 @@ export default {
fontSize: { fontSize: {
tiny: ['10px'], tiny: ['10px'],
sm: ['11px', '1.4'], sm: ['11px', '1.4'],
'annotator-sm': ['12px'],
base: ['13px', '1.4'], base: ['13px', '1.4'],
lg: ['14px'], lg: ['14px'],
xl: ['16px'], xl: ['16px'],
annotator: {
sm: ['12px'],
},
},
keyframes: {
adderPopDown: {
'0%': {
opacity: '0.05',
transform: 'scale(0.8) translateY(10px)',
},
'20%': {
opacity: '0.7',
},
'100%': {
opacity: '1',
transform: 'scale(1) translateY(0px)',
},
},
adderPopUp: {
'0%': {
opacity: '0.05',
transform: 'scale(0.8) translateY(-10px)',
},
'20%': {
opacity: '0.7',
},
'100%': {
opacity: '1',
transform: 'scale(1) translateY(0px)',
},
},
}, },
}, },
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment