diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b550f94..34b21eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `` - component for hiding elements in specific media - `` - - force children to get displayed as inline content + - force children to get displayed as inline content +- `` + - simple placeholder element that can be used to display info about missing data - `` - - `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly` + - `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly` +- `` + - `forceInline` property: force inline rendering +- `` + - `togglerSize`: replaces the deprecated `togglerLarge` property +- `: + - `searchListPredicate` property: Allows to filter the complete list of search options at once. + - Following optional BlueprintJs properties are forwarded now to override default behaviour: `noResults`, `createNewItemRenderer` and `itemRenderer` + - `isValidNewOption` property: Checks if an input string is or can be turned into a valid new option. ### Fixed @@ -21,9 +31,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - create more whitespace inside `small` tag - reduce visual impact of border - `` - - take Markdown rendering into account before testing the maximum preview length + - take Markdown rendering into account before testing the maximum preview length - `` - header-menu items are vertically centered now +- ``: + - border of the BlueprintJS `Tag` elements were fixed ### Changed @@ -37,11 +49,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `` - `` - `` and `` +- `` + - by default, if no searchPredicate or searchListPredicate is defined, the filtering is done via case-insensitive multi-word filtering. ### Deprecated - `` - - `firstNonEmptyLineOnly` will be removed, is replaced by `useOnly="firstNonEmptyLine"` + - `firstNonEmptyLineOnly` will be removed, is replaced by `useOnly="firstNonEmptyLine"` +- `` + - `togglerLarge`: replaced by the more versatile `togglerSize` property +- `` + - `searchPreficate`: replaced by the, in some cases, more efficient `searchListPredicate` ## [25.0.0] - 2025-12-01 diff --git a/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx b/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx index 26b214ab..91b9a06c 100644 --- a/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx +++ b/src/cmem/ContentBlobToggler/ContentBlobToggler.tsx @@ -1,6 +1,6 @@ -import React, { useState } from "react"; +import React, { useState} from "react"; -import { Link, Spacing } from "../../index"; +import { Link, Spacing, InlineText } from "../../index"; export interface ContentBlobTogglerProps extends React.HTMLAttributes { /** @@ -31,6 +31,10 @@ export interface ContentBlobTogglerProps extends React.HTMLAttributes {!isExtended ? ( <> @@ -76,7 +81,7 @@ export function ContentBlobToggler({ {fullviewContent} {enableToggler && (
- + {forceInline ? <>{" "} : } ); + + return forceInline ? {tooglerDisplay} : tooglerDisplay; } diff --git a/src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx b/src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx index e5ec7a79..08c19a43 100644 --- a/src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx +++ b/src/cmem/ContentBlobToggler/StringPreviewContentBlobToggler.tsx @@ -54,6 +54,7 @@ export function StringPreviewContentBlobToggler({ allowedHtmlElementsInPreview, noTogglerContentSuffix, firstNonEmptyLineOnly, + ...otherContentBlobTogglerProps }: StringPreviewContentBlobTogglerProps) { // need to test `firstNonEmptyLineOnly` until property is removed const useOnlyTest: StringPreviewContentBlobTogglerProps["useOnly"] = firstNonEmptyLineOnly @@ -105,6 +106,7 @@ export function StringPreviewContentBlobToggler({ fullviewContent={fullviewContent} startExtended={startExtended} enableToggler={enableToggler} + {...otherContentBlobTogglerProps} /> ); } diff --git a/src/components/Application/_header.scss b/src/components/Application/_header.scss index 27d7fd49..0a13abb0 100644 --- a/src/components/Application/_header.scss +++ b/src/components/Application/_header.scss @@ -52,19 +52,22 @@ $shell-header-icon-03: $eccgui-color-applicationheader-text !default; /// Item link $shell-header-link: $blue-60 !default; +/// Height +$shell-header-height: mini-units(8) !default; + // load library sub component @import "~@carbon/react/scss/components/ui-shell/header/index"; // tweak original layout .#{$prefix}--header { - height: mini-units(8); + height: $shell-header-height; } .#{$prefix}--header__action, .#{$prefix}--header__action.#{$prefix}--btn--icon-only { - width: mini-units(8); - height: mini-units(8); + width: $shell-header-height; + height: $shell-header-height; padding-block-start: 0; background-color: transparent; @@ -128,7 +131,7 @@ span.#{$prefix}--header__name { } .#{$prefix}--header__menu .#{$prefix}--header__menu-item { - height: mini-units(8); + height: $shell-header-height; } // tweak original colors (as long as config does not work properly) @@ -255,15 +258,15 @@ a.#{$prefix}--header__menu-item:focus > svg { // adjust position of all other modal dialogs .#{$ns}-dialog-container { - top: mini-units(8); - left: mini-units(8); - width: calc(100% - #{mini-units(8)}); - min-height: calc(100% - #{mini-units(8)}); + top: $shell-header-height; + left: $shell-header-height; + width: calc(100% - #{$shell-header-height}); + min-height: calc(100% - #{$shell-header-height}); } .#{$eccgui}-dialog__wrapper { - max-width: calc(100vw - #{mini-units(8)} - #{2 * $eccgui-size-block-whitespace}); - max-height: calc(100vh - #{mini-units(8)} - #{2 * $eccgui-size-block-whitespace}); + max-width: calc(100vw - #{$shell-header-height} - #{2 * $eccgui-size-block-whitespace}); + max-height: calc(100vh - #{$shell-header-height} - #{2 * $eccgui-size-block-whitespace}); margin: 0; } } diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index fcf25392..7efb0d02 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -104,13 +104,18 @@ export const Button = ({ const ButtonType = restProps.href ? BlueprintAnchorButton : BlueprintButton; + const iconSize = { + small: restProps["size"] === "small", + large: restProps["size"] === "large", + }; + const button = ( : icon} - rightIcon={typeof rightIcon === "string" ? : rightIcon} + icon={typeof icon === "string" ? : icon} + rightIcon={typeof rightIcon === "string" ? : rightIcon} > {children} {badge && ( diff --git a/src/components/ContextOverlay/ContextMenu.tsx b/src/components/ContextOverlay/ContextMenu.tsx index c51ca0b3..d0ec4bf3 100644 --- a/src/components/ContextOverlay/ContextMenu.tsx +++ b/src/components/ContextOverlay/ContextMenu.tsx @@ -2,7 +2,7 @@ import React, { ReactElement } from "react"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; import { ValidIconName } from "../Icon/canonicalIconNames"; -import IconButton from "../Icon/IconButton"; +import { IconButton, IconButtonProps } from "../Icon/IconButton"; import { TestableComponent } from "../interfaces"; import Menu from "../Menu/Menu"; @@ -28,8 +28,13 @@ export interface ContextMenuProps extends TestableComponent { * Text displayed as title or tooltip on toggler element. */ togglerText?: string; + /** + * Allow to de- and increase the size of the default toggler button. + */ + togglerSize?: IconButtonProps["size"]; /** * Toggler element is displayed larger than normal. + * @deprecated (v26) use `togglerSize="large" instead */ togglerLarge?: boolean; /** @@ -62,6 +67,7 @@ export const ContextMenu = ({ contextOverlayProps, disabled, togglerLarge = false, + togglerSize, /* FIXME: The Tooltip component can interfere with the opened menu, since it is implemented via portal and may cover the menu, so by default we use the title attribute instead of Tooltip. */ tooltipAsTitle = true, @@ -76,7 +82,7 @@ export const ContextMenu = ({ tooltipAsTitle={tooltipAsTitle} name={[togglerElement]} text={togglerText} - large={togglerLarge} + size={togglerLarge ? "large" : togglerSize} disabled={!!disabled} data-test-id={dataTestId ?? undefined} data-testid={dataTestid ?? undefined} diff --git a/src/components/Icon/IconButton.tsx b/src/components/Icon/IconButton.tsx index 969ffde2..1be9f885 100644 --- a/src/components/Icon/IconButton.tsx +++ b/src/components/Icon/IconButton.tsx @@ -54,8 +54,8 @@ export const IconButton = ({ swapPlaceholderDelay: 10, }; const iconProps = { - small: restProps.small, - large: restProps.large, + small: restProps.small || restProps["size"] === "small", + large: restProps.large || restProps["size"] === "large", tooltipText: tooltipAsTitle ? undefined : text, tooltipProps: tooltipProps ? { diff --git a/src/components/MultiSelect/MultiSelect.tsx b/src/components/MultiSelect/MultiSelect.tsx index 3766baf6..72af76fa 100644 --- a/src/components/MultiSelect/MultiSelect.tsx +++ b/src/components/MultiSelect/MultiSelect.tsx @@ -10,7 +10,15 @@ import { removeExtraSpaces } from "../../common/utils/stringUtils"; import { CLASSPREFIX as eccgui } from "../../configuration/constants"; import { TestableComponent } from "../interfaces"; -import { ContextOverlayProps, Highlighter, IconButton, MenuItem, OverflowText, Spinner } from "./../../index"; +import { + ContextOverlayProps, + Highlighter, + highlighterUtils, + IconButton, + MenuItem, + OverflowText, + Spinner +} from "./../../index"; export interface MultiSuggestFieldSelectionProps { newlySelected?: T; @@ -18,9 +26,10 @@ export interface MultiSuggestFieldSelectionProps { createdItems: Partial[]; } -interface MultiSuggestFieldCommonProps +export interface MultiSuggestFieldCommonProps extends TestableComponent, - Pick, "items" | "placeholder" | "openOnKeyDown"> { + Pick, "items" | "placeholder" | "openOnKeyDown" | "noResults" | "createNewItemRenderer">, + Partial, "itemRenderer">> { /** * Additional class name, space separated. */ @@ -70,9 +79,11 @@ interface MultiSuggestFieldCommonProps */ newItemCreationText?: string; /** - * Allows to creates new item from a given query. If this is not provided then no new items can be created. + * Allows to create new item from a given query. If this is not provided then no new items can be created. */ createNewItemFromQuery?: (query: string) => T; + /** Validates if a new item can be created from the current query string. */ + isValidNewOption?: (query: string) => boolean; /** * Items that were newly created and not taken from the list will be post-fixed with this string. */ @@ -103,9 +114,20 @@ interface MultiSuggestFieldCommonProps wrapperProps?: React.HTMLAttributes; /** * Function that allows us to filter values from the option list. - * If not provided, values are filtered by their labels + * + * @deprecated (v26) use `searchListPredicate` instead. */ searchPredicate?: (item: T, query: string) => boolean; + + /** + * Returns the filtered the search option list. + * By default, a case-insensitive multi-word filtering is applied. + * + * @param items The options. + * @param query The search query. + */ + searchListPredicate?: (items: T[], query: string) => T[] + /** * Limits the height of the input target plus its dropdown menu when it is opened. * Need to be a `number not greater than 100` (as `vh`, a unit describing a length relative to the viewport height) or `true` (equals 100). @@ -159,6 +181,7 @@ export function MultiSuggestField({ newItemPostfix = " (new item)", disabled, createNewItemFromQuery, + isValidNewOption, requestDelay = 0, clearQueryOnSelection = false, className, @@ -166,13 +189,14 @@ export function MultiSuggestField({ "data-testid": dataTestid, wrapperProps, searchPredicate, + searchListPredicate, limitHeightOpened, intent, ...otherMultiSelectProps }: MultiSuggestFieldProps) { // Options created by a user const createdItems = useRef([]); - // Options passed ouside (f.e. from the backend) + // Options passed outside (f.e. from the backend) const [externalItems, setExternalItems] = React.useState([...items]); // All options (created and passed) that match the query const [filteredItems, setFilteredItems] = React.useState([]); @@ -264,9 +288,14 @@ export function MultiSuggestField({ setSelectedItems(filteredItems); }; - const defaultFilterPredicate = (item: T, query: string) => { - return itemLabel(item).toLowerCase().includes(query); - }; + /** Does a case-insensitive multi-word search in the item label. */ + const defaultSearchListPredicate = (items: T[], query: string): T[] => { + const searchWords = highlighterUtils.extractSearchWords(query, true); + return items.filter(item => { + const searchIn = itemLabel(item).toLowerCase() + return highlighterUtils.matchesAllWords(searchIn, searchWords); + }) + } /** * selects and deselects an item from selection list @@ -305,10 +334,17 @@ export function MultiSuggestField({ if (requestState.current.query === query) { // Only use most recent request const outsideOptions = [...(resultFromQuery ?? externalItems)]; - const filter = searchPredicate ?? defaultFilterPredicate; + let itemFilter = defaultSearchListPredicate + if(searchListPredicate) { + itemFilter = searchListPredicate + } else if(searchPredicate) { + itemFilter = (items, query) => { + return items.filter((item) => searchPredicate(item, query)) + } + } setFilteredItems( - [...outsideOptions, ...createdItems.current].filter((item) => filter(item, query.toLowerCase())) + itemFilter([...outsideOptions, ...createdItems.current], query) ); setShowSpinner(false); } @@ -386,7 +422,9 @@ export function MultiSuggestField({ */ const handleOnKeyUp = (event: React.KeyboardEvent) => { if (event.key === "Enter" && !filteredItems.length && !!requestState.current.query && createNewItemFromQuery) { - createNewItem(requestState.current.query); + if(!isValidNewOption || isValidNewOption(requestState.current.query)) { + createNewItem(requestState.current.query); + } } inputRef.current?.focus(); }; @@ -402,7 +440,11 @@ export function MultiSuggestField({ if (focusedItem) { onItemSelect(focusedItem); } else { - onItemSelect(createNewItem(requestState.current.query)); + if (!isValidNewOption || isValidNewOption(requestState.current.query)) { + onItemSelect(createNewItem(requestState.current.query)); + } else { + return + } } requestState.current.query = ""; setTimeout(() => inputRef.current?.focus()); @@ -417,7 +459,7 @@ export function MultiSuggestField({ * @returns */ const newItemRenderer = (label: string, active: boolean, handleClick: React.MouseEventHandler) => { - if (!createNewItemFromQuery) return undefined; + if (!createNewItemFromQuery || isValidNewOption && !isValidNewOption(label)) return undefined; const clickHandler = (e: React.MouseEvent) => { createNewItem(label); handleClick(e); @@ -459,7 +501,6 @@ export function MultiSuggestField({ ? "Search for item, or enter term to create new one..." : undefined } - {...otherMultiSelectProps} query={requestState.current.query} onQueryChange={onQueryChange} items={filteredItems} @@ -528,6 +569,7 @@ export function MultiSuggestField({ : undefined, } as BlueprintMultiSelectProps["popoverContentProps"] } + {...otherMultiSelectProps} /> ); diff --git a/src/components/NotAvailable/NotAvailable.stories.tsx b/src/components/NotAvailable/NotAvailable.stories.tsx new file mode 100644 index 00000000..29dde1e3 --- /dev/null +++ b/src/components/NotAvailable/NotAvailable.stories.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; + +import { NotAvailable } from "../../../index"; + +export default { + title: "Components/NotAvailable", + component: NotAvailable, + argTypes: {}, +} as Meta; + +const TemplateFull: StoryFn = (args) => ; + +export const Default = TemplateFull.bind({}); +Default.args = {}; diff --git a/src/components/NotAvailable/NotAvailable.tsx b/src/components/NotAvailable/NotAvailable.tsx new file mode 100644 index 00000000..47b14adb --- /dev/null +++ b/src/components/NotAvailable/NotAvailable.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +import { + CLASSPREFIX as eccgui, + Tag, + TagProps, + Tooltip, + TooltipProps, +} from "../../../index"; +import { TestableComponent } from "../interfaces"; +export interface NotAvailableProps extends TestableComponent, Pick, Pick { + /** + * Text displayed by the element. + */ + label?: TagProps["children"]; + /** + * Add a tooltip to the element. + * You need to set an empty string `""` to remove it. + */ + tooltip?: TooltipProps["content"]; + /** + * Specify the display of the used `Tag` component. + */ + tagProps?: Omit; + /** + * Specify the display of the used `Tooltip` component. + */ + tooltipProps?: Omit; + /** + * Do not use the `Tag` component for the display. + * The `intent` state can be displayed only on the tooltip then. + */ + noTag?: boolean; +} + +/** + * Simple placeholder element that can be used to display info about missing data. + */ +export const NotAvailable = ({ + label = "n/a", + tooltip = "not available", + icon, + intent, + tagProps, + tooltipProps, + className, + noTag = false, + ...otherProps +}: NotAvailableProps) => { + const defaultTagProps : TagProps = { icon, intent, emphasis: "weaker", className: `${eccgui}-notavailable` + className ? ` ${className}` : "" }; + const naElement = noTag === false ? ( + + { label ?? "n/a"} + + ) : <>{ label ?? "n/a"}; + const defaultTooltipProps : TooltipProps = { + children: naElement, + content: tooltip, + intent, + addIndicator: noTag, + }; + + return tooltip ? : naElement; +}; + +export default NotAvailable; diff --git a/src/components/Tag/tag.scss b/src/components/Tag/tag.scss index 8abfb2e1..43f05b95 100644 --- a/src/components/Tag/tag.scss +++ b/src/components/Tag/tag.scss @@ -30,8 +30,6 @@ $tag-round-adjustment: 0 !default; @import "~@blueprintjs/core/src/components/tag/tag"; .#{$eccgui}-tag__item { - --eccgui-tag-border-width: 1px; - flex-grow: 0; flex-shrink: 0; min-width: calc(#{$tag-height} - 2px); @@ -141,6 +139,8 @@ $tag-round-adjustment: 0 !default; } .#{$ns}-tag { + --eccgui-tag-border-width: 1px; + border-style: solid; border-width: var(--eccgui-tag-border-width); diff --git a/src/components/Typography/typography.scss b/src/components/Typography/typography.scss index 87d20c59..623e4797 100644 --- a/src/components/Typography/typography.scss +++ b/src/components/Typography/typography.scss @@ -63,12 +63,14 @@ mark { line-height: $eccgui-size-typo-text-lineheight; } -.#{$eccgui}-typography__contentblock.#{$eccgui}-typography--small { +.#{$eccgui}-typography__contentblock.#{$eccgui}-typography--small, +.#{$eccgui}-typography--small { font-size: $eccgui-size-typo-caption; line-height: $eccgui-size-typo-caption-lineheight; } -.#{$eccgui}-typography__contentblock.#{$eccgui}-typography--large { +.#{$eccgui}-typography__contentblock.#{$eccgui}-typography--large, +.#{$eccgui}-typography--large { font-size: $eccgui-size-typo-subtitle; line-height: $eccgui-size-typo-subtitle-lineheight; } diff --git a/src/components/index.ts b/src/components/index.ts index 6f7fdf95..5e1c496d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -23,6 +23,7 @@ export * from "./Link/Link"; export * from "./List/List"; export * from "./Menu"; export * from "./MultiSuggestField"; +export * from "./NotAvailable/NotAvailable"; export * from "./Notification"; export * from "./OverviewItem"; export * from "./Pagination/Pagination";