Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EntityTitle component #6590

Merged
merged 16 commits into from
Jan 8, 2024
10 changes: 10 additions & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ export const EDITABLE_TEXT_EDITING = `${EDITABLE_TEXT}-editing`;
export const EDITABLE_TEXT_INPUT = `${EDITABLE_TEXT}-input`;
export const EDITABLE_TEXT_PLACEHOLDER = `${EDITABLE_TEXT}-placeholder`;

export const ENTITY_TITLE = `${NS}-entity-title`;
export const ENTITY_TITLE_ELLIPSIZE = `${NS}-entity-title-ellipsize`;
export const ENTITY_TITLE_HAS_SUBTITLE = `${ENTITY_TITLE}-has-subtitle`;
export const ENTITY_TITLE_ICON_CONTAINER = `${ENTITY_TITLE}-icon-container`;
export const ENTITY_TITLE_SUBTITLE = `${ENTITY_TITLE}-subtitle`;
export const ENTITY_TITLE_TAGS_CONTAINER = `${ENTITY_TITLE}-tags-container`;
export const ENTITY_TITLE_TEXT = `${ENTITY_TITLE}-text`;
export const ENTITY_TITLE_TITLE = `${ENTITY_TITLE}-title`;
export const ENTITY_TITLE_TITLE_AND_TAGS = `${ENTITY_TITLE}-title-and-tags`;

export const FLEX_EXPANDER = `${NS}-flex-expander`;

export const HTML_SELECT = `${NS}-html-select`;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@import "dialog/multistep-dialog";
@import "drawer/drawer";
@import "editable-text/editable-text";
@import "entity-title/entity-title";
@import "forms/index";
@import "html-select/html-select";
@import "html-table/html-table";
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@page collapse
@page divider
@page editable-text
@page entity-title
@page html
@page html-table
@page hotkeys-target2
Expand Down
91 changes: 91 additions & 0 deletions packages/core/src/components/entity-title/_entity-title.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2023 Palantir Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0.

@import "../../common/variables";
@import "../../common/variables-extended";
@import "../../typography";

.#{$ns}-entity-title {
align-items: center;
display: flex;
gap: 0.7 * $pt-grid-size;
min-width: 0;

&-icon-container {
&.#{$ns}-entity-title-has-subtitle {
align-self: flex-start;
}

&:not(.#{$ns}-entity-title-has-subtitle) {
align-items: center;
display: flex;
}
}

&-text {
display: flex;
flex-direction: column;
}

&-title-and-tags {
align-items: center;
display: flex;
flex-direction: row;
gap: 0.5 * $pt-grid-size;
}

&-tags-container {
display: flex;
gap: 0.2 * $pt-grid-size;
margin-left: 0.5 * $pt-grid-size;
}

&-title {
margin-bottom: 0;
min-width: 0;
overflow-wrap: break-word;
}

&-subtitle {
font-size: $pt-font-size-small;
margin-top: 0.2 * $pt-grid-size;
}

&-ellipsize,
&-ellipsize &-text {
overflow: hidden;
}

@each $tag, $props in $headings {
&-heading-#{$tag} .#{$ns}-icon-container {
align-items: center;
display: flex;
// Aligning icon which has unknown dimensions on the title size
height: nth($props, 2);
}
}

&-heading-h1,
&-heading-h2,
&-heading-h3 {
gap: 1.5 * $pt-grid-size;

.#{$ns}-entity-title-status-tag {
margin-left: $pt-grid-size;
}

.#{$ns}-entity-title-subtitle {
font-size: $pt-font-size;
}
}

&-heading-h4,
&-heading-h5,
&-heading-h6 {
gap: $pt-grid-size;

.#{$ns}-entity-title-subtitle {
font-size: $pt-font-size-small;
}
}
}
9 changes: 9 additions & 0 deletions packages/core/src/components/entity-title/entity-title.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@# Entity title

__EntityTitle__ is a component that handles rendering a common UI pattern consisting of title, icon, subtitle and tag.

@reactExample EntityTitleExample

@## Props interface

@interface EntityTitleProps
174 changes: 174 additions & 0 deletions packages/core/src/components/entity-title/entityTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* !
* Copyright 2023 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import classNames from "classnames";
import * as React from "react";

import { type IconName, IconNames } from "@blueprintjs/icons";

import { Classes, type MaybeElement, type Props } from "../../common";
import { H1, H2, H3, H4, H5, H6 } from "../html/html";
import { Icon } from "../icon/icon";
import { Text } from "../text/text";

export interface EntityTitleProps extends Props {
/**
* Whether the overflowing text content should be ellipsized.
*
* @default false
*/
ellipsize?: boolean;

/**
* React component to render the main title heading. This defaults to
* Blueprint's `<Text>` component, * which inherits font size from its
* containing element(s).
*
* To render larger, more prominent titles, Use Blueprint's heading
* components instead (e.g. `{ H1 } from "@blueprintjs/core"`).
*
* @default Text
*/
heading?: React.FC<any>;

/**
* Name of a Blueprint UI icon (or an icon element) to render in the section's header.
* Note that the header will only be rendered if `title` is provided.
*/
icon?: IconName | MaybeElement;

/**
* Whether to render as loading state.
*
* @default false
*/
loading?: boolean;

/** The content to render below the title. Defaults to render muted text. */
subtitle?: JSX.Element | string;

/** The primary title to render. */
title: JSX.Element | string;

/** If specified, the title will be wrapped in an anchor with this URL. */
titleURL?: string;

/**
* <Tag> components work best - if multiple, wrap in <React.Fragment>
*/
tags?: React.ReactNode;
}

/**
* EntityTitle component.
*
* @see https://blueprintjs.com/docs/#core/components/entity-title
*/
export const EntityTitle: React.FC<EntityTitleProps> = ({
adidahiya marked this conversation as resolved.
Show resolved Hide resolved
className,
ellipsize = false,
heading = Text,
icon,
loading = false,
subtitle,
tags,
title,
titleURL,
}) => {
const titleElement = React.useMemo(() => {
const maybeTitleWithURL =
titleURL != null ? (
<a target="_blank" href={titleURL} rel="noreferrer">
{title}
</a>
) : (
title
);

return React.createElement(
heading,
{
className: classNames(Classes.ENTITY_TITLE_TITLE, {
[Classes.SKELETON]: loading,
[Classes.TEXT_OVERFLOW_ELLIPSIS]: heading !== Text && ellipsize,
}),
ellipsize: heading === Text ? ellipsize : undefined,
},
maybeTitleWithURL,
);
}, [titleURL, title, heading, loading, ellipsize]);

const maybeSubtitle = React.useMemo(() => {
if (subtitle == null) {
return null;
}

return (
<Text
className={classNames(Classes.TEXT_MUTED, Classes.ENTITY_TITLE_SUBTITLE, {
[Classes.SKELETON]: loading,
})}
ellipsize={ellipsize}
>
{subtitle}
</Text>
);
}, [ellipsize, loading, subtitle]);

return (
<div
className={classNames(className, Classes.ENTITY_TITLE, getClassNameFromHeading(heading), {
[Classes.ENTITY_TITLE_ELLIPSIZE]: ellipsize,
})}
>
{icon != null && (
<div
className={classNames(Classes.ENTITY_TITLE_ICON_CONTAINER, {
[Classes.ENTITY_TITLE_HAS_SUBTITLE]: maybeSubtitle != null,
})}
>
<Icon
aria-hidden={true}
className={classNames(Classes.TEXT_MUTED, { [Classes.SKELETON]: loading })}
icon={loading ? IconNames.SQUARE : icon}
tabIndex={-1}
/>
</div>
)}
<div className={Classes.ENTITY_TITLE_TEXT}>
<div
className={classNames(Classes.ENTITY_TITLE_TITLE_AND_TAGS, {
[Classes.SKELETON]: loading,
})}
>
{titleElement}
{tags != null && <div className={Classes.ENTITY_TITLE_TAGS_CONTAINER}>{tags}</div>}
</div>
{maybeSubtitle}
</div>
</div>
);
};

// Construct header class name from H{*}. Returns `undefined` if `heading` is
// not a Blueprint heading.
function getClassNameFromHeading(heading: React.FC<any>) {
const headerIndex = [H1, H2, H3, H4, H5, H6].findIndex(header => header === heading);
if (headerIndex < 0) {
return undefined;
}
return [Classes.getClassNamespace(), "entity-title-heading", `h${headerIndex + 1}`].join("-");
}
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export { EditableText, type EditableTextProps } from "./editable-text/editableTe
export { ControlGroup, type ControlGroupProps } from "./forms/controlGroup";
export { Checkbox, type CheckboxProps, Radio, type RadioProps, Switch, type SwitchProps } from "./forms/controls";
export type { ControlProps } from "./forms/controlProps";
export { EntityTitle, type EntityTitleProps } from "./entity-title/entityTitle";
export { FileInput, type FileInputProps } from "./forms/fileInput";
export { FormGroup, type FormGroupProps } from "./forms/formGroup";
export { InputGroup, type InputGroupProps } from "./forms/inputGroup";
Expand Down
Loading