diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.scss b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.scss index 0ea66ba28b2..f433cbe89a6 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.scss +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.scss @@ -46,9 +46,6 @@ nav.controlsRow .chart-controls .settings-menu { // chart-type label // .settings-menu-header { - display: flex; - justify-content: space-between; - align-items: center; background: white; padding: 9px $indent 3px $indent; position: sticky; diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index c4720e52cdd..bc96df1c7a0 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -30,6 +30,7 @@ import { TableFilterToggle, TableFilterToggleManager, } from "./settings/TableFilterToggle" +import { OverlayHeader } from "../core/OverlayHeader" const { LineChart, @@ -315,12 +316,11 @@ export class SettingsMenu extends React.Component<{ ...this.layout, }} > -
-
- {menuTitle} -
- this.toggleVisibility()} /> -
+
{this.sidePanelBounds && ( - + )} @@ -2625,7 +2621,6 @@ export class Grapher {/* entity selector in a slide-in drawer */} { this.isEntitySelectorModalOrDrawerOpen = @@ -3115,14 +3110,6 @@ export class Grapher : timeColumn.formatValue(value) } - @computed get entitySelectorTitle(): string { - return this.canHighlightEntities - ? `Select ${this.entityTypePlural}` - : this.canChangeEntity - ? `Choose ${a(this.entityType)}` - : `Add/remove ${this.entityTypePlural}` - } - @computed get canSelectMultipleEntities(): boolean { if (this.numSelectableEntityNames < 2) return false if (this.addCountryMode === EntitySelectionMode.MultipleEntities) diff --git a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts index f747ca28119..c882b8ab32b 100644 --- a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts +++ b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts @@ -6,9 +6,8 @@ export const GRAPHER_EMBEDDED_FIGURE_CONFIG_ATTR = "data-grapher-config" export const GRAPHER_PAGE_BODY_CLASS = "StandaloneGrapherOrExplorerPage" export const GRAPHER_DRAWER_ID = "grapher-drawer" export const GRAPHER_IS_IN_IFRAME_CLASS = "IsInIframe" -export const GRAPHER_SCROLLABLE_CONTAINER_CLASS = "scrollable-container" export const GRAPHER_TIMELINE_CLASS = "timeline-component" -export const GRAPHER_ENTITY_SELECTOR_CLASS = "entity-selector" +export const GRAPHER_SIDE_PANEL_CLASS = "side-panel" export const DEFAULT_GRAPHER_CONFIG_SCHEMA = "https://files.ourworldindata.org/schemas/grapher-schema.004.json" diff --git a/packages/@ourworldindata/grapher/src/core/OverlayHeader.scss b/packages/@ourworldindata/grapher/src/core/OverlayHeader.scss new file mode 100644 index 00000000000..bde2d7e1743 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/core/OverlayHeader.scss @@ -0,0 +1,12 @@ +.overlay-header { + --padding: var(--modal-padding, 16px); + + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--padding) var(--padding) 16px; + + button { + margin-left: 8px; + } +} diff --git a/packages/@ourworldindata/grapher/src/core/OverlayHeader.tsx b/packages/@ourworldindata/grapher/src/core/OverlayHeader.tsx new file mode 100644 index 00000000000..6157a7ab2a5 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/core/OverlayHeader.tsx @@ -0,0 +1,20 @@ +import React from "react" +import cx from "classnames" +import { CloseButton } from "../closeButton/CloseButton.js" + +export function OverlayHeader({ + title, + onDismiss, + className, +}: { + title: string + onDismiss?: () => void + className?: string +}): JSX.Element { + return ( +
+

{title}

+ {onDismiss && } +
+ ) +} diff --git a/packages/@ourworldindata/grapher/src/core/grapher.scss b/packages/@ourworldindata/grapher/src/core/grapher.scss index d93becc8294..24134673c2c 100644 --- a/packages/@ourworldindata/grapher/src/core/grapher.scss +++ b/packages/@ourworldindata/grapher/src/core/grapher.scss @@ -90,6 +90,7 @@ $zindex-controls-drawer: 150; @import "../closeButton/CloseButton.scss"; @import "../controls/RadioButton.scss"; @import "../controls/Dropdown.scss"; +@import "../core/OverlayHeader.scss"; .grapher_dark { color: $dark-text; diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss index d0df2e59099..85b6898cbda 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss @@ -2,28 +2,109 @@ .entity-selector { --padding: var(--modal-padding, 16px); - $row-border: 1px solid #f2f2f2; - - $footer-height: 48px; + color: $dark-text; + + // necessary for scrolling + display: flex; + flex-direction: column; + height: 100%; + > * { + flex-shrink: 0; + } - $zindex-header: 2; - $zindex-footer: 2; - $zindex-animated: 1; + .scrollable { + flex: 1 1 auto; + overflow-y: auto; + } .entity-selector__search-bar { - position: sticky; - top: 0; - left: 0; - background-color: #fff; - z-index: $zindex-header; padding: 0 var(--padding) 8px var(--padding); - margin-bottom: 8px; + + .search-input { + // search icon + $svg-margin: 8px; + $svg-size: 12px; + + $placeholder: #a1a1a1; + $focus: 1px solid #a4b6ca; + + position: relative; + + .search-icon { + position: absolute; + top: 50%; + left: $svg-margin; + color: $light-text; + transform: translateY(-50%); + font-size: $svg-size; + } + + .clear { + margin: 0; + padding: 0; + background: none; + border: none; + position: absolute; + top: 50%; + right: $svg-margin; + transform: translateY(-50%); + font-size: $svg-size; + color: $dark-text; + cursor: pointer; + } + + input[type="search"] { + @include grapher_label-2-regular; + width: 100%; + height: 32px; + border: 1px solid #e7e7e7; + padding-left: $svg-margin + $svg-size + 4px; + padding-right: $svg-margin + $svg-size + 4px; + border-radius: 4px; + background: #fff; + + // style placeholder text in search input + &::placeholder { + color: $placeholder; + opacity: 1; /* Firefox */ + } + &:-ms-input-placeholder { + color: $placeholder; + } + &::-ms-input-placeholder { + color: $placeholder; + } + + // style focus state + &:focus { + outline: none; + border: $focus; + } + &:focus:not(:focus-visible) { + border: none; + } + &:focus-visible { + border: $focus; + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + } + } + + &.search-input--empty { + input[type="search"] { + padding-right: 8px; + } + } + } } .entity-selector__sort-bar { padding: 0 var(--padding); display: flex; align-items: center; + margin-top: 8px; margin-bottom: 16px; .grapher-dropdown { @@ -79,222 +160,132 @@ } } - .entity-selector__footer { - position: absolute; - bottom: 0; - left: 0; - background-color: #fff; - padding: 0 16px; - width: 100%; - border-radius: 4px; - z-index: $zindex-footer; - - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 var(--padding); - height: $footer-height; - box-shadow: 0px -4px 8px 0px rgba(0, 0, 0, 0.04); + .entity-selector__content { + $row-border: 1px solid #f2f2f2; - .footer__selected { - font-size: 0.6875rem; - font-weight: 500; - letter-spacing: 0.06em; - text-transform: uppercase; + margin: 0 var(--padding) 8px var(--padding); - // small visual correction - top: 1px; - position: relative; - - &--no-wrap { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + .entity-section + .entity-section { + margin-top: 16px; } - button { - background: none; - border: none; - color: $dark-text; - font-size: 0.8125rem; - font-weight: 500; + .entity-section__title { letter-spacing: 0.01em; - text-decoration-line: underline; - text-underline-offset: 3px; - cursor: pointer; - - &:disabled { - color: #c6c6c6; - text-decoration: none; - cursor: default; - } + margin-bottom: 8px; } - } - .entity-selector__content { - margin: 0 var(--padding) ($footer-height + 8px) var(--padding); - } - - &.entity-selector--single { - .entity-selector__content { - margin-bottom: 16px; + .entity-search-results { + margin-top: 8px; } - } - .entity-section + .entity-section { - margin-top: 16px; - } - - .entity-section__title { - letter-spacing: 0.01em; - margin-bottom: 8px; - } - - .selectable-entity { - padding: 9px 8px 9px 16px; - display: flex; - justify-content: space-between; - position: relative; - cursor: pointer; + .selectable-entity { + padding: 9px 8px 9px 16px; + display: flex; + justify-content: space-between; + position: relative; + cursor: pointer; - .value { - color: #a1a1a1; - white-space: nowrap; - margin-left: 8px; - } + .value { + color: #a1a1a1; + white-space: nowrap; + margin-left: 8px; + } - .bar { - position: absolute; - top: 0; - left: 0; - height: 100%; - background: #ebeef2; - z-index: -1; - } + .bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: #ebeef2; + z-index: -1; + } - .label-with-location-icon { - display: flex; - align-items: center; + .label-with-location-icon { + display: flex; + align-items: center; - svg { - margin-left: 8px; - font-size: 0.9em; - color: #a1a1a1; + svg { + margin-left: 8px; + font-size: 0.9em; + color: #a1a1a1; - // hide focus outline when clicked - &:focus:not(:focus-visible) { - outline: none; + // hide focus outline when clicked + &:focus:not(:focus-visible) { + outline: none; + } } } } - } - .animated-entity { - position: relative; - z-index: 0; - background: #fff; + .animated-entity { + position: relative; + z-index: 0; + background: #fff; - &.most-recently-selected { - z-index: $zindex-animated; + &.most-recently-selected { + z-index: 1; + } } - } - li + li .selectable-entity { - border-top: $row-border; - } + ul { + margin: 0; + padding: 0; + list-style-type: none; + } - li:first-of-type .selectable-entity { - border-top: $row-border; - } + li + li .selectable-entity { + border-top: $row-border; + } - li:last-of-type .selectable-entity { - border-bottom: $row-border; + li:first-of-type .selectable-entity { + border-top: $row-border; + } + + li:last-of-type .selectable-entity { + border-bottom: $row-border; + } } - .search-input { - // search icon - $svg-margin: 8px; - $svg-size: 12px; + .entity-selector__footer { + background-color: #fff; + width: 100%; + border-radius: 4px; + z-index: 2; - $placeholder: #a1a1a1; - $focus: 1px solid #a4b6ca; + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px var(--padding); + box-shadow: 0px -4px 8px 0px rgba(0, 0, 0, 0.04); - position: relative; + .footer__selected { + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; - .search-icon { - position: absolute; - top: 50%; - left: $svg-margin; - color: $light-text; - transform: translateY(-50%); - font-size: $svg-size; + display: flex; + flex-wrap: wrap; + column-gap: 4px; } - .clear { - margin: 0; - padding: 0; + button { background: none; border: none; - position: absolute; - top: 50%; - right: $svg-margin; - transform: translateY(-50%); - font-size: $svg-size; color: $dark-text; + font-size: 0.8125rem; + font-weight: 500; + letter-spacing: 0.01em; + text-decoration-line: underline; + text-underline-offset: 3px; cursor: pointer; - } - - input[type="search"] { - @include grapher_label-2-regular; - width: 100%; - height: 32px; - border: 1px solid #e7e7e7; - padding-left: $svg-margin + $svg-size + 4px; - padding-right: $svg-margin + $svg-size + 4px; - border-radius: 4px; - background: #fff; - // style placeholder text in search input - &::placeholder { - color: $placeholder; - opacity: 1; /* Firefox */ - } - &:-ms-input-placeholder { - color: $placeholder; - } - &::-ms-input-placeholder { - color: $placeholder; - } - - // style focus state - &:focus { - outline: none; - border: $focus; - } - &:focus:not(:focus-visible) { - border: none; - } - &:focus-visible { - border: $focus; - } - - &::-webkit-search-cancel-button { - -webkit-appearance: none; - } - } - - &.search-input--empty { - input[type="search"] { - padding-right: 8px; + &:disabled { + color: #c6c6c6; + text-decoration: none; + cursor: default; } } } - - ul { - margin: 0; - padding: 0; - list-style-type: none; - } } } diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx index dea9c805a7a..3f8f76a61f7 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx @@ -36,8 +36,6 @@ import { makeSelectionArray } from "../chart/ChartUtils.js" import { DEFAULT_GRAPHER_ENTITY_TYPE, DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL, - GRAPHER_ENTITY_SELECTOR_CLASS, - GRAPHER_SCROLLABLE_CONTAINER_CLASS, POPULATION_INDICATOR_ID_USED_IN_ENTITY_SELECTOR, GDP_PER_CAPITA_INDICATOR_ID_USED_IN_ENTITY_SELECTOR, isPopulationVariableId, @@ -49,6 +47,8 @@ import { scaleLinear, type ScaleLinear } from "d3-scale" import { ColumnSlug } from "@ourworldindata/types" import { buildVariableTable } from "../core/LegacyToOwidTable" import { loadVariableDataAndMetadata } from "../core/loadVariable" +import { OverlayHeader } from "../core/OverlayHeader.js" +import { DrawerContext } from "../slideInDrawer/SlideInDrawer.js" export interface EntitySelectorState { searchInput: string @@ -64,11 +64,13 @@ export interface EntitySelectorManager { entitySelectorState: Partial tableForSelection: OwidTable selection: SelectionArray - canChangeEntity: boolean entityType?: string entityTypePlural?: string activeColumnSlugs?: string[] dataApiUrl: string + isEntitySelectorModalOrDrawerOpen?: boolean + canChangeEntity?: boolean + canHighlightEntities?: boolean } interface SortConfig { @@ -105,7 +107,9 @@ export class EntitySelector extends React.Component<{ onDismiss?: () => void autoFocus?: boolean }> { - container: React.RefObject = React.createRef() + static contextType = DrawerContext + + scrollableContainer: React.RefObject = React.createRef() searchField: React.RefObject = React.createRef() private defaultSortConfig = { @@ -119,15 +123,12 @@ export class EntitySelector extends React.Component<{ if (this.props.autoFocus && !isTouchDevice()) this.searchField.current?.focus() - const scrollableContainer = this.container.current?.closest( - `.${GRAPHER_SCROLLABLE_CONTAINER_CLASS}` - ) - // scroll to the top when the search input changes reaction( () => this.searchInput, () => { - if (scrollableContainer) scrollableContainer.scrollTop = 0 + if (this.scrollableContainer.current) + this.scrollableContainer.current.scrollTop = 0 } ) } @@ -216,6 +217,14 @@ export class EntitySelector extends React.Component<{ return this.props.manager } + @computed private get title(): string { + return this.manager.canHighlightEntities + ? `Select ${this.entityTypePlural}` + : this.manager.canChangeEntity + ? `Choose ${a(this.entityType)}` + : `Add/remove ${this.entityTypePlural}` + } + @computed private get searchInput(): string { return this.manager.entitySelectorState.searchInput ?? "" } @@ -679,6 +688,16 @@ export class EntitySelector extends React.Component<{ this.toggleSortOrder() } + @action.bound private onDismiss(): void { + // if rendered into a drawer, we use a method provided by the + // `` component so that closing the drawer is animated + if (this.context.toggleDrawerVisibility) { + this.context.toggleDrawerVisibility() + } else { + this.manager.isEntitySelectorModalOrDrawerOpen = false + } + } + private renderSearchBar(): JSX.Element { return (
@@ -744,7 +763,7 @@ export class EntitySelector extends React.Component<{ private renderSearchResults(): JSX.Element { if (!this.searchResults || this.searchResults.length === 0) { return ( -
+
There is no data for the {this.entityType} you are looking for. You may want to try using different keywords or checking for typos. @@ -836,7 +855,7 @@ export class EntitySelector extends React.Component<{
{selected.length > 0 && ( -
+
Selection
@@ -939,10 +958,17 @@ export class EntitySelector extends React.Component<{ ) : ( -
- {selectedEntityNames.length > 0 - ? `Current selection: ${selectedEntityNames[0]}` - : "Empty selection"} +
+ {selectedEntityNames.length > 0 ? ( + <> + Current selection: + + {selectedEntityNames[0]} + + + ) : ( + "Empty selection" + )}
)}
@@ -951,24 +977,26 @@ export class EntitySelector extends React.Component<{ render(): JSX.Element { return ( -
- {this.renderSearchBar()} +
+ - {!this.searchInput && - this.sortOptions.length > 1 && - this.renderSortBar()} + {this.renderSearchBar()} -
- {this.searchInput - ? this.renderSearchResults() - : this.isMultiMode - ? this.renderAllEntitiesInMultiMode() - : this.renderAllEntitiesInSingleMode()} +
+ {!this.searchInput && + this.sortOptions.length > 1 && + this.renderSortBar()} + +
+ {this.searchInput + ? this.renderSearchResults() + : this.isMultiMode + ? this.renderAllEntitiesInMultiMode() + : this.renderAllEntitiesInSingleMode()} +
{this.renderFooter()} diff --git a/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss b/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss index cafd18859c5..07e180b9137 100644 --- a/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss +++ b/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss @@ -1,8 +1,23 @@ .download-modal-content { color: $dark-text; - padding: 0 var(--modal-padding) var(--modal-padding); - min-height: 45px; - position: relative; + + // necessary for scrolling + display: flex; + flex-direction: column; + height: 100%; + > * { + flex-shrink: 0; + } + + .scrollable { + flex: 1 1 auto; + overflow-y: auto; + padding: 0 var(--modal-padding) var(--modal-padding); + + // needed for the loading indicator + position: relative; + min-height: 45px; + } .grouped-menu-section { h3 { diff --git a/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx b/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx index 2f8d0f5fa94..1529a82091d 100644 --- a/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx +++ b/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx @@ -20,6 +20,7 @@ import { } from "@ourworldindata/core-table" import { Modal } from "./Modal" import { GrapherExport } from "../captionedChart/StaticChartRasterizer.js" +import { OverlayHeader } from "../core/OverlayHeader.js" export interface DownloadModalManager { displaySlug: string @@ -362,17 +363,22 @@ export class DownloadModal extends React.Component { render(): JSX.Element { return ( - -
- {this.isReady ? ( - this.renderReady() - ) : ( - - )} + +
+ +
+ {this.isReady ? ( + this.renderReady() + ) : ( + + )} +
) diff --git a/packages/@ourworldindata/grapher/src/modal/EmbedModal.scss b/packages/@ourworldindata/grapher/src/modal/EmbedModal.scss index f88795663ac..954228136fc 100644 --- a/packages/@ourworldindata/grapher/src/modal/EmbedModal.scss +++ b/packages/@ourworldindata/grapher/src/modal/EmbedModal.scss @@ -1,6 +1,19 @@ .embed-modal-content { color: $dark-text; - margin: 0 var(--modal-padding) var(--modal-padding); + + // necessary for scrolling + display: flex; + flex-direction: column; + height: 100%; + > * { + flex-shrink: 0; + } + + .scrollable { + flex: 1 1 auto; + overflow-y: auto; + padding: 0 var(--modal-padding) var(--modal-padding); + } p { margin-bottom: 16px; diff --git a/packages/@ourworldindata/grapher/src/modal/EmbedModal.tsx b/packages/@ourworldindata/grapher/src/modal/EmbedModal.tsx index ab6bc066943..0baa0cf011a 100644 --- a/packages/@ourworldindata/grapher/src/modal/EmbedModal.tsx +++ b/packages/@ourworldindata/grapher/src/modal/EmbedModal.tsx @@ -4,6 +4,7 @@ import { computed, action } from "mobx" import { Bounds, DEFAULT_BOUNDS } from "@ourworldindata/utils" import { Modal } from "./Modal" import { CodeSnippet } from "@ourworldindata/components" +import { OverlayHeader } from "../core/OverlayHeader.js" export interface EmbedModalManager { canonicalUrl?: string @@ -45,17 +46,22 @@ export class EmbedModal extends React.Component { render(): JSX.Element { return ( -
-

- Paste this into any HTML page: -

- - {this.manager.embedDialogAdditionalElements} +
+ +
+

+ Paste this into any HTML page: +

+ + {this.manager.embedDialogAdditionalElements} +
) diff --git a/packages/@ourworldindata/grapher/src/modal/EntitySelectorModal.tsx b/packages/@ourworldindata/grapher/src/modal/EntitySelectorModal.tsx index a6604f4140e..8ecbb0867da 100644 --- a/packages/@ourworldindata/grapher/src/modal/EntitySelectorModal.tsx +++ b/packages/@ourworldindata/grapher/src/modal/EntitySelectorModal.tsx @@ -11,7 +11,6 @@ import { export interface EntitySelectorModalManager extends EntitySelectorManager { isEntitySelectorModalOrDrawerOpen?: boolean frameBounds?: Bounds - entitySelectorTitle?: string } @observer @@ -39,7 +38,6 @@ export class EntitySelectorModal extends React.Component<{ render(): JSX.Element { return ( void - title?: string children?: React.ReactNode isHeightFixed?: boolean // by default, the modal height is not fixed but fits to the content alignVertical?: "center" | "bottom" - showStickyHeader?: boolean }> { contentRef: React.RefObject = React.createRef() @@ -22,10 +17,6 @@ export class Modal extends React.Component<{ return this.props.bounds } - @computed private get title(): string | undefined { - return this.props.title - } - @computed private get isHeightFixed(): boolean { return this.props.isHeightFixed ?? false } @@ -34,10 +25,6 @@ export class Modal extends React.Component<{ return this.props.alignVertical ?? "center" } - @computed private get showStickyHeader(): boolean { - return this.props.showStickyHeader || !!this.title - } - @action.bound onDocumentClick(e: MouseEvent): void { const tagName = (e.target as HTMLElement).tagName const isTargetInteractive = ["A", "BUTTON", "INPUT"].includes(tagName) @@ -95,27 +82,7 @@ export class Modal extends React.Component<{ style={contentStyle} ref={this.contentRef} > - {this.showStickyHeader ? ( -
-

- {this.title} -

- -
- ) : ( - - )} -
- {this.props.children} -
+ {this.props.children}
diff --git a/packages/@ourworldindata/grapher/src/modal/SourcesModal.scss b/packages/@ourworldindata/grapher/src/modal/SourcesModal.scss index 9a636d7a837..a9f015a9c56 100644 --- a/packages/@ourworldindata/grapher/src/modal/SourcesModal.scss +++ b/packages/@ourworldindata/grapher/src/modal/SourcesModal.scss @@ -7,12 +7,36 @@ $border: #e7e7e7; - max-width: $max-content-width; - margin: 0 auto; - padding: 0 var(--modal-padding) var(--modal-padding); + // necessary for scrolling + display: flex; + flex-direction: column; + height: 100%; + > * { + flex-shrink: 0; + } + + .scrollable { + max-width: $max-content-width; + margin: 0 auto; + + flex: 1 1 auto; + overflow-y: auto; + padding: 0 var(--modal-padding) var(--modal-padding); + + &--pad-top { + padding-top: var(--modal-padding); + } + + // needed for the loading indicator + position: relative; + min-height: 45px; + } - &.sources-modal-content--pad-top { - margin-top: var(--modal-padding); + .close-button--top-right { + position: absolute; + top: 0; + right: 0; + margin: var(--modal-padding); } .note-multiple-indicators { diff --git a/packages/@ourworldindata/grapher/src/modal/SourcesModal.tsx b/packages/@ourworldindata/grapher/src/modal/SourcesModal.tsx index bc3b6ed66f0..d4c034cff3b 100644 --- a/packages/@ourworldindata/grapher/src/modal/SourcesModal.tsx +++ b/packages/@ourworldindata/grapher/src/modal/SourcesModal.tsx @@ -36,8 +36,9 @@ import { SourcesDescriptions } from "./SourcesDescriptions" import { Tabs } from "../tabs/Tabs" import { ExpandableTabs } from "../tabs/ExpandableTabs" import { LoadingIndicator } from "../loadingIndicator/LoadingIndicator" -import { CLOSE_BUTTON_WIDTH } from "../closeButton/CloseButton" +import { CLOSE_BUTTON_WIDTH, CloseButton } from "../closeButton/CloseButton" import { isContinentsVariableId } from "../core/GrapherConstants" +import { OverlayHeader } from "../core/OverlayHeader.js" // keep in sync with variables in SourcesModal.scss const MAX_CONTENT_WIDTH = 640 @@ -92,7 +93,7 @@ export class SourcesModal extends React.Component< return this.frameBounds.padHeight(15).padWidth(padWidth) } - @computed private get showStickyModalHeader(): boolean { + @computed private get showStickyHeader(): boolean { const modalWidth = this.modalBounds.width - 2 * this.modalPadding return (modalWidth - MAX_CONTENT_WIDTH) / 2 < CLOSE_BUTTON_WIDTH + 2 } @@ -262,19 +263,27 @@ export class SourcesModal extends React.Component< bounds={this.modalBounds} isHeightFixed={true} onDismiss={this.onDismiss} - showStickyHeader={this.showStickyModalHeader} > -
- {this.manager.isReady ? ( - this.renderModalContent() +
+ {this.showStickyHeader ? ( + ) : ( - + )} +
+ {this.manager.isReady ? ( + this.renderModalContent() + ) : ( + + )} +
) diff --git a/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.scss b/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.scss index a0b4a725640..14cc01d4ea6 100644 --- a/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.scss +++ b/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.scss @@ -3,19 +3,8 @@ flex-shrink: 0; border-left: 1px solid $frame-color; - position: relative; - - // necessary for scrolling - display: flex; - flex-direction: column; - - .side-panel__header { - margin: 16px; - flex-shrink: 0; // necessary for scrolling - } - - .side-panel__scrollable { - flex-grow: 1; - overflow-y: auto; + // don't show close button in the side panel header + .overlay-header .close-button { + display: none; } } diff --git a/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.tsx b/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.tsx index d12567f2d01..59d748692b8 100644 --- a/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.tsx +++ b/packages/@ourworldindata/grapher/src/sidePanel/SidePanel.tsx @@ -1,14 +1,12 @@ import React from "react" -import cx from "classnames" import { observer } from "mobx-react" import { computed } from "mobx" import { Bounds } from "@ourworldindata/utils" -import { GRAPHER_SCROLLABLE_CONTAINER_CLASS } from "../core/GrapherConstants" +import { GRAPHER_SIDE_PANEL_CLASS } from "../core/GrapherConstants.js" @observer export class SidePanel extends React.Component<{ bounds: Bounds - title: string children: React.ReactNode }> { @computed private get bounds(): Bounds { @@ -18,23 +16,13 @@ export class SidePanel extends React.Component<{ render(): JSX.Element { return (
-

- {this.props.title} -

-
- {this.props.children} -
+ {this.props.children}
) } diff --git a/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.scss b/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.scss index 1649aa6a87b..84b40363286 100644 --- a/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.scss +++ b/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.scss @@ -16,35 +16,7 @@ width: 300px; height: 100vh; z-index: $zindex-controls-drawer; - overflow-y: scroll; background: white; - - // necessary for scrolling - display: flex; - flex-direction: column; - - .grapher-drawer-header { - position: static; - display: flex; - justify-content: space-between; - align-items: center; - background: white; - padding: 16px; - position: sticky; - top: 0; - z-index: 1; - - flex-shrink: 0; // necessary for scrolling - - button { - margin-left: 8px; - } - } - - .grapher-drawer-scrollable { - overflow-y: auto; - flex-grow: 1; - } } .grapher-drawer-backdrop { diff --git a/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.tsx b/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.tsx index a763f36a0b3..2a17cdee73b 100644 --- a/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.tsx +++ b/packages/@ourworldindata/grapher/src/slideInDrawer/SlideInDrawer.tsx @@ -1,17 +1,15 @@ import React from "react" -import cx from "classnames" import { createPortal } from "react-dom" import { computed, action, observable } from "mobx" import { observer } from "mobx-react" -import { - GRAPHER_DRAWER_ID, - GRAPHER_SCROLLABLE_CONTAINER_CLASS, -} from "../core/GrapherConstants" -import { CloseButton } from "../closeButton/CloseButton.js" +import { GRAPHER_DRAWER_ID } from "../core/GrapherConstants" + +export const DrawerContext = React.createContext<{ + toggleDrawerVisibility?: () => void +}>({}) @observer export class SlideInDrawer extends React.Component<{ - title: string active: boolean toggle: () => void children: React.ReactNode @@ -87,21 +85,13 @@ export class SlideInDrawer extends React.Component<{ ...this.animationFor("grapher-drawer-contents"), }} > -
-
- {this.props.title} -
- this.toggleVisibility()} /> -
- -
{this.props.children} -
+
) diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 14d1225ea19..59c548c491d 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -35,7 +35,7 @@ import { GRAPHER_FONT_SCALE_10_5, GRAPHER_FONT_SCALE_11_2, GRAPHER_TIMELINE_CLASS, - GRAPHER_ENTITY_SELECTOR_CLASS, + GRAPHER_SIDE_PANEL_CLASS, } from "../core/GrapherConstants" import { ScaleType, @@ -536,7 +536,7 @@ export class SlopeChart const target = e.target as HTMLElement // check if the target is an interactive element or contained within one - const selector = `a, button, input, .${GRAPHER_TIMELINE_CLASS}, .${GRAPHER_ENTITY_SELECTOR_CLASS}` + const selector = `a, button, input, .${GRAPHER_TIMELINE_CLASS}, .${GRAPHER_SIDE_PANEL_CLASS}` const isTargetInteractive = target.closest(selector) !== null if (