Skip to content

Commit

Permalink
✨ (entity selector) design feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Apr 16, 2024
1 parent 58508ee commit e11f882
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

color: $light-text;
background: white;
border: 1px solid $light-stroke !important;
border: 1px solid $light-stroke;
border-radius: 50%;

&:hover {
background: $hover-fill;
border-color: $hover-fill;
cursor: pointer;
color: $dark-text;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@ourworldindata/grapher/src/core/OverlayHeader.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
align-items: center;
padding: var(--padding) var(--padding) 16px;

button {
.close-button {
margin-left: 8px;
flex-shrink: 0;
}
}
11 changes: 10 additions & 1 deletion packages/@ourworldindata/grapher/src/core/OverlayHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import { CloseButton } from "../closeButton/CloseButton.js"

export function OverlayHeader({
title,
onTitleClick,
onDismiss,
className,
}: {
title: string
onTitleClick?: () => void
onDismiss?: () => void
className?: string
}): JSX.Element {
return (
<div className={cx("overlay-header", className)}>
<h2 className="grapher_h5-black-caps grapher_light">{title}</h2>
<h2
className={cx("grapher_h5-black-caps", "grapher_light", {
clickable: !!onTitleClick,
})}
onClick={onTitleClick}
>
{title}
</h2>
{onDismiss && <CloseButton onClick={onDismiss} />}
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.entity-selector {
--padding: var(--modal-padding, 16px);

$sort-button-size: 32px;
$sort-button-margin: 16px;

color: $dark-text;

// necessary for scrolling
Expand Down Expand Up @@ -114,22 +117,19 @@
.label {
flex-shrink: 0;
margin-right: 8px;
color: $dark-text;
}

button.sort {
flex-shrink: 0;
margin-left: 16px;

$size: 32px;
margin-left: $sort-button-margin;

display: flex;
align-items: center;
justify-content: center;

position: relative;
height: $size;
width: $size;
height: $sort-button-size;
width: $sort-button-size;
padding: 7px;

color: $dark-text;
Expand Down Expand Up @@ -185,6 +185,14 @@
position: relative;
cursor: pointer;

&.hovered {
background: rgba(219, 229, 240, 0.4);
}

&--with-bar.hovered {
background: rgba(219, 229, 240, 0.6);
}

.value {
color: #a1a1a1;
white-space: nowrap;
Expand Down Expand Up @@ -263,10 +271,6 @@
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;

display: flex;
flex-wrap: wrap;
column-gap: 4px;
}

button {
Expand All @@ -287,4 +291,8 @@
}
}
}

.grapher-dropdown .menu {
width: calc(100% + $sort-button-margin + $sort-button-size);
}
}
147 changes: 97 additions & 50 deletions packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react"
import React, { useRef } from "react"
import { observer } from "mobx-react"
import { computed, action, reaction } from "mobx"
import cx from "classnames"
Expand Down Expand Up @@ -112,6 +112,8 @@ export class EntitySelector extends React.Component<{
scrollableContainer: React.RefObject<HTMLDivElement> = React.createRef()
searchField: React.RefObject<HTMLInputElement> = React.createRef()

private dismissTimer?: NodeJS.Timeout

private defaultSortConfig = {
slug: this.table.entityNameSlug,
order: SortOrder.asc,
Expand All @@ -133,6 +135,10 @@ export class EntitySelector extends React.Component<{
)
}

componentWillUnmount(): void {
if (this.dismissTimer) clearTimeout(this.dismissTimer)
}

private set(newState: Partial<EntitySelectorState>): void {
const correctedState = { ...newState }

Expand Down Expand Up @@ -568,6 +574,11 @@ export class EntitySelector extends React.Component<{
)
}

@action.bound onTitleClick(): void {
if (this.scrollableContainer.current)
this.scrollableContainer.current.scrollTop = 0
}

@action.bound onSearchKeyDown(e: React.KeyboardEvent<HTMLElement>): void {
const { searchResults } = this
if (e.key === "Enter" && searchResults && searchResults.length > 0) {
Expand All @@ -581,7 +592,11 @@ export class EntitySelector extends React.Component<{
this.selectionArray.toggleSelection(entityName)
} else {
this.selectionArray.setSelectedEntities([entityName])
if (this.props.onDismiss) this.props.onDismiss()
if (this.props.onDismiss) {
this.dismissTimer = setTimeout(() => {
if (this.props.onDismiss) this.props.onDismiss()
}, 250)
}
}

this.set({ mostRecentlySelectedEntityName: entityName })
Expand Down Expand Up @@ -739,7 +754,9 @@ export class EntitySelector extends React.Component<{
private renderSortBar(): JSX.Element {
return (
<div className="entity-selector__sort-bar">
<span className="label grapher_label-2-medium">Sort by</span>
<span className="label grapher_label-2-medium grapher_light">
Sort by
</span>
<Dropdown
options={this.sortOptions}
onChange={this.onChangeSortSlug}
Expand Down Expand Up @@ -790,21 +807,56 @@ export class EntitySelector extends React.Component<{
}

private renderAllEntitiesInSingleMode(): JSX.Element {
const { selected, unselected } = this.partitionedAvailableEntities
return (
<ul>
{this.sortedAvailableEntities.map((entity) => (
<li key={entity.name}>
<SelectableEntity
name={entity.name}
type="radio"
checked={this.isEntitySelected(entity)}
bar={this.getBarConfigForEntity(entity)}
onChange={() => this.onChange(entity.name)}
local={entity.local}
/>
</li>
))}
</ul>
<Flipper
spring={{
stiffness: 300,
damping: 33,
}}
flipKey={this.selectionArray.selectedEntityNames.join(",")}
>
<ul>
{selected.map((entity) => (
<Flipped
key={entity.name}
flipId={entity.name}
translate
opacity
>
<li key={entity.name}>
<SelectableEntity
name={entity.name}
type="radio"
checked={true}
bar={this.getBarConfigForEntity(entity)}
onChange={() => this.onChange(entity.name)}
local={entity.local}
/>
</li>
</Flipped>
))}
{unselected.map((entity) => (
<Flipped
key={entity.name}
flipId={entity.name}
translate
opacity
>
<li key={entity.name}>
<SelectableEntity
name={entity.name}
type="radio"
checked={false}
bar={this.getBarConfigForEntity(entity)}
onChange={() => this.onChange(entity.name)}
local={entity.local}
/>
</li>
</Flipped>
))}
</ul>
</Flipper>
)
}

Expand Down Expand Up @@ -937,48 +989,35 @@ export class EntitySelector extends React.Component<{
}

private renderFooter(): JSX.Element {
const { numSelectedEntities, selectedEntityNames } = this.selectionArray
const { numSelectedEntities } = this.selectionArray
const { partitionedVisibleEntities: visibleEntities } = this

return (
<div className="entity-selector__footer">
{this.isMultiMode ? (
<>
<div className="footer__selected">
{numSelectedEntities > 0
? `${numSelectedEntities} selected`
: "Empty selection"}
</div>
<button
type="button"
onClick={this.onClear}
disabled={visibleEntities.selected.length === 0}
>
Clear
</button>
</>
) : (
<div className="footer__selected">
{selectedEntityNames.length > 0 ? (
<>
Current selection:
<span className="entity-name">
{selectedEntityNames[0]}
</span>
</>
) : (
"Empty selection"
)}
</div>
)}
<div className="footer__selected">
{numSelectedEntities > 0
? `${numSelectedEntities} selected`
: "Empty selection"}
</div>
<button
type="button"
onClick={this.onClear}
disabled={visibleEntities.selected.length === 0}
>
Clear
</button>
</div>
)
}

render(): JSX.Element {
return (
<div className="entity-selector">
<OverlayHeader title={this.title} onDismiss={this.onDismiss} />
<OverlayHeader
title={this.title}
onTitleClick={this.onTitleClick}
onDismiss={this.onDismiss}
/>

{this.renderSearchBar()}

Expand All @@ -996,7 +1035,7 @@ export class EntitySelector extends React.Component<{
</div>
</div>

{this.renderFooter()}
{this.isMultiMode && this.renderFooter()}
</div>
)
}
Expand All @@ -1019,6 +1058,8 @@ function SelectableEntity({
onChange: () => void
local?: boolean
}) {
const element = useRef<HTMLDivElement>(null)

const Input = {
checkbox: Checkbox,
radio: RadioButton,
Expand All @@ -1041,7 +1082,13 @@ function SelectableEntity({

return (
<div
className="selectable-entity"
ref={element}
className={cx("selectable-entity", {
"selectable-entity--with-bar": bar && bar.width !== undefined,
})}
// can't use :hover because an element keeps its hover style while it's animated
onMouseEnter={() => element.current?.classList.add("hovered")}
onMouseLeave={() => element.current?.classList.remove("hovered")}
// make the whole row clickable
onClickCapture={(e) => {
e.stopPropagation()
Expand Down

0 comments on commit e11f882

Please sign in to comment.