Commit 51270f0e authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Refactor SASS for sizes, variants, clarity

* Break out most private mixins into `_base`
* Provide `IconButton`, `LabeledButton`, `LinkButton` as public mixins
* Establish colors for all variants in `_config`
* Extend mixins to support sizes and more variants
* Update `TopBar` and `SearchInput`, as custom styling no longer
  necessary; remove those custom styles
parent d3ef7988
...@@ -80,9 +80,9 @@ export default function SearchInput({ alwaysExpanded, query, onSearch }) { ...@@ -80,9 +80,9 @@ export default function SearchInput({ alwaysExpanded, query, onSearch }) {
{!isLoading && ( {!isLoading && (
<div className="SearchInput__button-container"> <div className="SearchInput__button-container">
<IconButton <IconButton
className="CompactIconButton"
icon="search" icon="search"
onClick={() => input.current.focus()} onClick={() => input.current.focus()}
size="small"
title="Search annotations" title="Search annotations"
/> />
</div> </div>
......
...@@ -5,9 +5,8 @@ import isThirdPartyService from '../helpers/is-third-party-service'; ...@@ -5,9 +5,8 @@ import isThirdPartyService from '../helpers/is-third-party-service';
import { withServices } from '../service-context'; import { withServices } from '../service-context';
import { applyTheme } from '../helpers/theme'; import { applyTheme } from '../helpers/theme';
import { IconButton } from '../../shared/components/buttons'; import { IconButton, LinkButton } from '../../shared/components/buttons';
import Button from './Button';
import GroupList from './GroupList'; import GroupList from './GroupList';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';
import SortMenu from './SortMenu'; import SortMenu from './SortMenu';
...@@ -83,20 +82,24 @@ function TopBar({ ...@@ -83,20 +82,24 @@ function TopBar({
<span className="TopBar__login-links"></span> <span className="TopBar__login-links"></span>
)} )}
{auth.status === 'logged-out' && ( {auth.status === 'logged-out' && (
<span className="TopBar__login-links"> <span className="TopBar__login-links u-font--large u-horizontal-rhythm">
<Button <LinkButton
className="TopBar__login-button" className="InlineLinkButton"
buttonText="Sign up"
onClick={onSignUp} onClick={onSignUp}
style={loginLinkStyle} style={loginLinkStyle}
/>{' '} variant="primary"
/ >
<Button Sign up
className="TopBar__login-button" </LinkButton>
buttonText="Log in" <div>/</div>
<LinkButton
className="InlineLinkButton"
onClick={onLogin} onClick={onLogin}
style={loginLinkStyle} style={loginLinkStyle}
/> variant="primary"
>
Log in
</LinkButton>
</span> </span>
)} )}
{auth.status === 'logged-in' && ( {auth.status === 'logged-in' && (
...@@ -113,10 +116,10 @@ function TopBar({ ...@@ -113,10 +116,10 @@ function TopBar({
<StreamSearchInput /> <StreamSearchInput />
<div className="u-stretch" /> <div className="u-stretch" />
<IconButton <IconButton
className="CompactIconButton"
icon="help" icon="help"
expanded={isHelpPanelOpen} expanded={isHelpPanelOpen}
onClick={requestHelp} onClick={requestHelp}
size="small"
title="Help" title="Help"
/> />
{loginControl} {loginControl}
...@@ -129,9 +132,9 @@ function TopBar({ ...@@ -129,9 +132,9 @@ function TopBar({
<div className="u-stretch" /> <div className="u-stretch" />
{pendingUpdateCount > 0 && ( {pendingUpdateCount > 0 && (
<IconButton <IconButton
className="CompactIconButton"
icon="refresh" icon="refresh"
onClick={applyPendingUpdates} onClick={applyPendingUpdates}
size="small"
variant="primary" variant="primary"
title={`Show ${pendingUpdateCount} new/updated ${ title={`Show ${pendingUpdateCount} new/updated ${
pendingUpdateCount === 1 ? 'annotation' : 'annotations' pendingUpdateCount === 1 ? 'annotation' : 'annotations'
...@@ -145,18 +148,18 @@ function TopBar({ ...@@ -145,18 +148,18 @@ function TopBar({
<SortMenu /> <SortMenu />
{showSharePageButton && ( {showSharePageButton && (
<IconButton <IconButton
className="CompactIconButton"
icon="share" icon="share"
expanded={isAnnotationsPanelOpen} expanded={isAnnotationsPanelOpen}
onClick={toggleSharePanel} onClick={toggleSharePanel}
size="small"
title="Share annotations on this page" title="Share annotations on this page"
/> />
)} )}
<IconButton <IconButton
className="CompactIconButton"
icon="help" icon="help"
expanded={isHelpPanelOpen} expanded={isHelpPanelOpen}
onClick={requestHelp} onClick={requestHelp}
size="small"
title="Help" title="Help"
/> />
{loginControl} {loginControl}
......
...@@ -147,13 +147,10 @@ describe('TopBar', () => { ...@@ -147,13 +147,10 @@ describe('TopBar', () => {
onSignUp, onSignUp,
}); });
const loginText = getLoginText(wrapper); const loginText = getLoginText(wrapper);
const loginButtons = loginText.find('Button'); const loginButtons = loginText.find('LinkButton');
assert.equal(loginButtons.length, 2); assert.equal(loginButtons.length, 2);
assert.equal(loginButtons.at(0).props().buttonText, 'Sign up');
assert.equal(loginButtons.at(0).props().onClick, onSignUp); assert.equal(loginButtons.at(0).props().onClick, onSignUp);
assert.equal(loginButtons.at(1).props().buttonText, 'Log in');
assert.equal(loginButtons.at(1).props().onClick, onLogin); assert.equal(loginButtons.at(1).props().onClick, onLogin);
}); });
......
@use "sass:map";
@use "@hypothesis/frontend-shared/styles/mixins/focus";
// Set colors for a button
@mixin _colors($colormap) {
color: map.get($colormap, 'foreground');
background-color: map.get($colormap, 'background');
&:disabled {
color: map.get($colormap, 'disabled-foreground');
}
}
// Set hover colors and transition for a button
@mixin _hover-state($colormap) {
&:hover:not([disabled]) {
color: map.get($colormap, 'hover-foreground');
background-color: map.get($colormap, 'hover-background');
}
transition: color 0.2s ease-out, background-color 0.2s ease-out,
opacity 0.2s ease-out;
}
// Set active state colors for a button
@mixin _active-state($colormap) {
&[aria-expanded='true'],
&[aria-pressed='true'] {
color: map.get($colormap, 'active-foreground');
@if map.get($colormap, 'active-background') {
background-color: map.get($colormap, 'active-background');
}
&:hover:not([disabled]) {
color: map.get($colormap, 'hover-foreground');
}
&:focus:not([disabled]) {
color: map.get($colormap, 'active-foreground');
}
}
}
// Variant mixin: may be be used by variants (BEM modifier classes)
@mixin button--variant($options) {
@include _colors(map.get($options, 'colormap'));
@if map.get($options, 'withStates') {
@include _active-state(map.get($options, 'colormap'));
@include _hover-state(map.get($options, 'colormap'));
}
@content;
}
// Base mixin for buttons.
@mixin button($options) {
// Reset browser defaults
@include focus.outline-on-keyboard-focus;
padding: 0;
margin: 0;
background-color: transparent;
border-style: none;
@include _colors(map.get($options, 'colormap'));
@if map.get($options, 'withStates') {
@include _active-state(map.get($options, 'colormap'));
@include _hover-state(map.get($options, 'colormap'));
}
border-radius: map.get($options, 'border-radius');
border: none;
padding: 0.5em;
&--small {
padding: 0.25em;
}
&--large {
padding: 0.75em;
}
font-size: 1em;
font-weight: 700;
white-space: nowrap; // Keep multi-word button labels from wrapping
@if map.get($options, 'inline') {
display: inline;
} @else {
display: flex;
justify-content: center;
align-items: center;
}
@if map.get($options, 'withLayout') {
&--icon-left svg {
margin-right: map.get($options, 'margin');
}
&--icon-right svg {
margin-left: map.get($options, 'margin');
}
// When a button has "layout", that indicates it has some textual content:
// Size text to the contextual 1em, and adjust the icon to look balanced.
// H frontend app buttons tend to apply an icon:text ratio of ~1.25:1
svg {
width: 1.25em;
height: 1.25em;
}
} @else {
// In the case where an icon is the only content in a <button> element,
// size the icon based on contextual font-size. i.e. the icon IS
// the content
svg {
width: 1em;
height: 1em;
}
}
}
...@@ -7,6 +7,7 @@ $touch-target-size: var.$touch-target-size !default; ...@@ -7,6 +7,7 @@ $touch-target-size: var.$touch-target-size !default;
$color-g1: var.$color-grey-1 !default; $color-g1: var.$color-grey-1 !default;
$color-g2: var.$color-grey-2 !default; $color-g2: var.$color-grey-2 !default;
$color-g3: var.$color-grey-3 !default; $color-g3: var.$color-grey-3 !default;
$color-g4: var.$color-grey-4 !default;
$color-g6: var.$color-grey-6 !default; $color-g6: var.$color-grey-6 !default;
$color-g7: var.$color-grey-7 !default; $color-g7: var.$color-grey-7 !default;
$color-gsemi: var.$color-grey-semi !default; $color-gsemi: var.$color-grey-semi !default;
...@@ -14,62 +15,96 @@ $color-gmid: var.$color-grey-mid !default; ...@@ -14,62 +15,96 @@ $color-gmid: var.$color-grey-mid !default;
$color-brand: var.$color-brand !default; $color-brand: var.$color-brand !default;
$color-link-hover: var.$color-link-hover !default; $color-link-hover: var.$color-link-hover !default;
// The following SASS maps define color sets for each type of button // Colors for labeled buttons
$LabeledButton-colors: (
// Default colors for buttons
$colors: (
'foreground': $color-gmid, 'foreground': $color-gmid,
'background': $color-g1, 'background': $color-g1,
'hover-foreground': $color-g7, 'hover-foreground': $color-g7,
'hover-background': $color-g2, 'hover-background': $color-g2,
'active-foreground': $color-g7, 'active-foreground': $color-g7,
'active-background': $color-g1,
'border': transparent,
'disabled-foreground': $color-gmid, 'disabled-foreground': $color-gmid,
); );
// Icon-only buttons have a transparent background by default, and a more // Variant currently unused
// visible active (pressed) state $LabeledButton-colors--light: $LabeledButton-colors;
$IconButton-colors: map.merge(
$colors, $LabeledButton-colors--primary: (
( 'foreground': $color-g1,
'background': transparent, 'background': $color-gmid,
'hover-background': transparent, 'hover-foreground': $color-g1,
'active-foreground': $color-brand, 'hover-background': $color-g6,
'active-background': transparent, 'active-foreground': $color-g1,
) 'disabled-foreground': $color-g4,
);
$LabeledButton-colors--dark: (
'foreground': $color-gmid,
'background': transparent,
'hover-foreground': $color-g7,
'hover-background': $color-g3,
'active-foreground': $color-gmid,
'active-background': $color-g3,
'disabled-foreground': $color-gmid,
);
// Colors for icon-only buttons
$IconButton-colors: (
'foreground': $color-gmid,
'background': transparent,
'hover-foreground': $color-g7,
'hover-background': transparent,
'active-foreground': $color-brand,
'disabled-foreground': $color-gmid,
);
$IconButton-colors--light: (
'foreground': $color-gsemi,
'background': transparent,
'hover-foreground': $color-g6,
'hover-background': transparent,
'active-foreground': $color-gsemi,
'disabled-foreground': $color-gsemi,
);
$IconButton-colors--primary: (
'foreground': $color-brand,
'background': transparent,
'hover-foreground': $color-brand,
'hover-background': transparent,
'active-foreground': $color-brand,
'disabled-foreground': $color-gmid,
); );
$IconButton-colors--primary: map.merge( // Variant currently unused
$IconButton-colors, $IconButton-colors--dark: $IconButton-colors;
(
'foreground': $color-brand, // Colors for buttons styled as "links"
'hover-foreground': $color-brand, $LinkButton-colors: (
'active-foreground': $color-brand, 'foreground': $color-gmid,
) 'background': transparent,
'hover-foreground': $color-link-hover,
'hover-background': transparent,
'active-foreground': $color-g7,
'disabled-foreground': $color-gmid,
); );
$LabeledButton-colors: $colors; // Variant currently unused
$LinkButton-colors--light: $LinkButton-colors;
// This set of colors is light text on dark background $LinkButton-colors--primary: (
$LabeledButton-colors--primary: map.merge( 'foreground': $color-brand,
$colors, 'background': transparent,
( 'hover-foreground': $color-brand,
'foreground': $color-g1, 'hover-background': transparent,
'background': $color-gmid, 'active-foreground': $color-brand,
'hover-foreground': $color-g1, 'disabled-foreground': $color-gmid,
'hover-background': $color-g6,
'disabled-foreground': $color-gsemi,
)
); );
// This set of colors is for buttons styled as links on a white background $LinkButton-colors--dark: (
$LinkButton-colors: map.merge( 'foreground': $color-g7,
$colors, 'background': transparent,
( 'hover-foreground': $color-link-hover,
'background': transparent, 'hover-background': transparent,
'hover-background': transparent, 'active-foreground': $color-g7,
'active-background': transparent, 'disabled-foreground': $color-gmid,
'hover-foreground': $color-link-hover,
)
); );
@use "sass:map";
@use "../../variables" as var;
@use './_config' as *; // Bring variables into scope
@use './mixins'; @use './mixins';
// A button with only an icon and no label/text
.IconButton {
@include mixins.Button(
$options: (
'colormap': $IconButton-colors,
)
) {
@media (pointer: coarse) {
min-width: $touch-target-size;
min-height: $touch-target-size;
}
}
&--primary {
@include mixins.Button--variant(
$options: (
'colormap': $IconButton-colors--primary,
)
);
}
}
// A button with text, and optionally an icon // A button with text, and optionally an icon
.LabeledButton { .LabeledButton {
@include mixins.Button( @include mixins.LabeledButton;
$options: ( }
'colormap': $LabeledButton-colors,
'withLayout': true, // A button with only an icon and no label/text
) .IconButton {
); @include mixins.IconButton;
&--primary {
@include mixins.Button--variant(
$options: (
'colormap': $LabeledButton-colors--primary,
)
);
}
} }
// A button styled to appear as a link, with underline on hover // A button styled to appear as a link, with underline on hover
.LinkButton { .LinkButton {
@include mixins.Button( @include mixins.LinkButton;
$options: (
'colormap': $LinkButton-colors,
'withStates': false,
)
) {
// Override base font-weight
font-weight: 400;
}
// No active states, but we do want a hover state
@include mixins.hover-state($LinkButton-colors) {
text-decoration: underline;
}
// Remove horizontal padding to allow button to appear flush-left or -right
padding: 0.5em 0;
} }
...@@ -2,174 +2,141 @@ ...@@ -2,174 +2,141 @@
@use "@hypothesis/frontend-shared/styles/mixins/focus"; @use "@hypothesis/frontend-shared/styles/mixins/focus";
@use './_config' as *; @use './_config' as c;
@use './_base' as base;
// Reset browser native styles. This can be used in conjunction with other
// styles via `base` or standalone for starting a custom button's styles
// from relative scratch
@mixin reset {
@include focus.outline-on-keyboard-focus;
// Reset native styles
padding: 0;
margin: 0;
background-color: transparent;
border-style: none;
}
// Set colors for a button // Allow access to the configuration values to users of this module (esp. colors)
@mixin _colors($colormap) { @forward './_config';
color: map.get($colormap, 'foreground');
background-color: map.get($colormap, 'background');
&:disabled { // Base mixin for <button> elements
color: map.get($colormap, 'disabled-foreground'); @mixin Button($options: ()) {
} $defaultOptions: (
} // What colors should be used for this button's styling?
'colormap': c.$LabeledButton-colors,
// And for its variants...needed if `withVariants` is true (true is default)
'colormap--primary': c.$LabeledButton-colors--primary,
'colormap--dark': c.$LabeledButton-colors--dark,
'colormap--light': c.$LabeledButton-colors--light,
// Should this button apply an inline layout? (default is flex)
'inline': false,
// Should styling be added for "active" and "hover" states for this <button>?
'withStates': true,
// Should styling be added to support the layout of multiple sub-elements
// of this <button>? Not needed if <button> contains only one child.
'withLayout': false,
// Provide styling for light, primary and dark variants? If this is true,
// make sure all variant colormaps are provided
'withVariants': true,
// Internal margin around SVG icon
'margin': 0.5em,
'border-radius': 2px
);
$-options: map.merge($defaultOptions, $options);
// Set hover colors and transition for a button @include base.button($options: $-options);
@mixin hover-state($colormap) {
&:hover:not([disabled]),
&:focus:not([disabled]) {
color: map.get($colormap, 'hover-foreground');
background-color: map.get($colormap, 'hover-background');
@content;
}
transition: color 0.2s ease-out, background-color 0.2s ease-out,
opacity 0.2s ease-out;
}
// Set active state colors for a button // Add styles for supported variants as modifier classes, if `withVariants` enabled
@mixin active-state($colormap) { @if (map.get($-options, 'withVariants')) {
&[aria-expanded='true'], &--light {
&[aria-pressed='true'] { $-light-options: (
color: map.get($colormap, 'active-foreground'); 'colormap': map.get($-options, 'colormap--light'),
background-color: map.get($colormap, 'active-background'); 'withStates': map.get($-options, 'withStates'),
@content; );
@include base.button--variant($-light-options);
&:hover:not([disabled]), }
&:focus:not([disabled]) {
color: map.get($colormap, 'active-foreground'); &--primary {
background-color: map.get($colormap, 'active-background'); $-primary-options: (
@content; 'colormap': map.get($-options, 'colormap--primary'),
'withStates': map.get($-options, 'withStates'),
);
@include base.button--variant($-primary-options);
} }
}
}
// Variant mixin: may be be used by variants (BEM modifier classes) &--dark {
@mixin _variant($options) { $-dark-options: (
@include _colors(map.get($options, 'colormap')); 'colormap': map.get($-options, 'colormap--dark'),
@if map.get($options, 'withStates') { 'withStates': map.get($-options, 'withStates'),
@include active-state(map.get($options, 'colormap')); );
@include hover-state(map.get($options, 'colormap')); @include base.button--variant($-dark-options);
}
} }
@content; @content;
} }
// Base mixin for buttons. // Base mixin for a button with an icon and no label/content. Supports
@mixin _button($options) { // variants and sizes. Will assert responsive touch-target sizing in
@include reset; // --medium (default) and --large variants.
@include _colors(map.get($options, 'colormap')); @mixin IconButton($options: ()) {
$defaultOptions: (
@if map.get($options, 'withStates') { 'colormap': c.$IconButton-colors,
@include active-state(map.get($options, 'colormap')); 'colormap--light': c.$IconButton-colors--light,
@include hover-state(map.get($options, 'colormap')); 'colormap--primary': c.$IconButton-colors--primary,
} 'colormap--dark': c.$IconButton-colors--dark,
'responsive': true,
border-radius: map.get($options, 'border-radius'); );
border: none; $-options: map.merge($defaultOptions, $options);
padding: 0.5em;
font-size: 1em;
font-weight: 700;
white-space: nowrap; // Keep multi-word button labels from wrapping
display: flex;
justify-content: center;
align-items: center;
@if map.get($options, 'withLayout') {
&--icon-left svg {
margin-right: map.get($options, 'margin');
}
&--icon-right svg { @include Button($-options) {
margin-left: map.get($options, 'margin'); // Establish a minimum touch-target for touch devices if 'responsive'
} // option is enabled (default). This is not applied to the `--small`
// When a button has "layout", that indicates it has some textual content: // size variant.
// Size text to the contextual 1em, and adjust the icon to look balanced. @if map.get($-options, 'responsive') {
// H frontend app buttons tend to apply an icon:text ratio of ~1.25:1 @media (pointer: coarse) {
svg { &--medium,
width: 1.25em; &--large {
height: 1.25em; min-width: c.$touch-target-size;
} min-height: c.$touch-target-size;
} @else { }
// In the case where an icon is the only content in a <button> element, }
// size the icon based on contextual font-size. i.e. the icon IS
// the content
svg {
width: 1em;
height: 1em;
} }
} }
}
/** @content;
* Convenience function to retrieve a colormap. This obviates the need to reach
* into the `_config` module directly from outside and is less verbose.
*
* @param {'colors'|'icon'|'icon--primary'|'labeled'|'labeled--primary'|'link'}
* [$name] - Shorthand name of colormap to retrieved. Defaults to base
* $colors.
* @return {sass:map}
*/
@function colormap($name: 'colors') {
$result: $colors;
@if $name == 'icon' {
$result: $IconButton-colors;
}
@if $name == 'icon--primary' {
$result: $IconButton-colors--primary;
}
@if $name == 'labeled' {
$result: $LabeledButton-colors;
}
@if $name == 'labeled--primary' {
$result: $LabeledButton-colors--primary;
}
@if $name == 'link' {
$result: $LinkButton-colors;
}
@return $result;
} }
// Base mixin for <button> elements. Start here for new <button> classes // Base mixin for a button that has text/content and, optionally, an icon.
@mixin Button($options: ()) { // Supports variants and sizes.
@mixin LabeledButton($options: ()) {
$defaultOptions: ( $defaultOptions: (
// What colors should be used for this button's styling? 'colormap': c.$LabeledButton-colors,
'colormap': $colors, 'colormap--light': c.$LabeledButton-colors--light,
// Should styling be added for "active" and "hover" states for this <button>? 'colormap--primary': c.$LabeledButton-colors--primary,
'withStates': true, 'colormap--dark': c.$LabeledButton-colors--dark,
// Should styling be added to support the layout of multiple sub-elements 'withLayout': true,
// of this <button>? Not needed if <button> contains only one child.
'withLayout': false,
// Internal margin around SVG icon
'margin': 0.5em,
'border-radius': 2px
); );
$-options: map.merge($defaultOptions, $options); $-options: map.merge($defaultOptions, $options);
@include _button($options: $-options);
@include Button($-options);
@content; @content;
} }
// Mixin for --modifier variants on buttons, e.g. `.MyButton--primary` // Base mixin for a button styled to look like an <a> link. Supports variants
@mixin Button--variant($options: ()) { // but not pressed/active states at present.
@mixin LinkButton($options: ()) {
$defaultOptions: ( $defaultOptions: (
// Colors to use when styling this variant 'colormap': c.$LinkButton-colors,
'colormap': $colors, 'colormap--light': c.$LinkButton-colors--light,
// Should this variant have styling for "active" and "hover" states? 'colormap--primary': c.$LinkButton-colors--primary,
'withStates': true 'colormap--dark': c.$LinkButton-colors--dark,
); );
$-options: map.merge($defaultOptions, $options); $-options: map.merge($defaultOptions, $options);
@include _variant($options: $-options); @include Button($-options) {
// Lighter font weight for link-styled buttons
font-weight: 400;
// Add an underline on hover
&:hover:not([disabled]) {
text-decoration: underline;
}
&--primary {
// Primary variant has bolder text (and also different colors)
font-weight: 500;
}
}
// Remove padding to allow button to appear flush with surrounding content
padding: 0;
@content; @content;
} }
// Button styling for the sidebar extending common button-component styles // Button styling for the sidebar extending common button-component styles
@use '../shared/components/buttons/mixins' as buttons; @use '../shared/components/buttons/mixins' as buttons;
// Similar to `.IconButton`, with these changes: // Similar to `.LinkButton`, with inline layout (so button can be used
// - omit responsive minimum sizing // within text)
// - tighten padding .InlineLinkButton {
.CompactIconButton { @include buttons.LinkButton(
// Use icon colors for base and primary variants (
$base-options: ( 'inline': true,
'colormap': buttons.colormap('icon'), )
); );
$primary-options: ( // Custom: The dark variant is used for inline, anchor-like styling and needs
'colormap': buttons.colormap('icon--primary'), // to be underlined at all times to match <a> styling near it
); &--dark {
text-decoration: underline;
@include buttons.Button($options: $base-options) {
padding: 0.25em; // Override padding
}
&--primary {
@include buttons.Button--variant($options: $primary-options);
} }
} }
...@@ -50,6 +50,10 @@ ...@@ -50,6 +50,10 @@
@include layout.vertical-rhythm; @include layout.vertical-rhythm;
} }
.u-font--large {
@include utils.font--large;
}
.u-font--xlarge { .u-font--xlarge {
@include utils.font--xlarge; @include utils.font--xlarge;
} }
......
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