diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbaef6d6..bdaf6182e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [Unreleased] +### Added + +- `` + - component for hiding elements in specific media + +### Changed + +- automatically hide user interaction elements in print view + - all application header components except `` + - `` and `` + - `actionOptions` of `` + - `actions` of `` + - `` +- automatically serialize display of layout elements in print view + - `` + - `` + - `` and `` + ## [25.0.0] - 2025-12-01 This is a major release, and it might be not compatible with your current usage of our library. Please read about the necessary changes in the section about how to migrate. diff --git a/src/components/Application/ApplicationViewability.tsx b/src/components/Application/ApplicationViewability.tsx new file mode 100644 index 000000000..c9dafcc34 --- /dev/null +++ b/src/components/Application/ApplicationViewability.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import classNames from "classnames"; + +import { CLASSPREFIX as eccgui } from "../../configuration/constants"; + +type media = "print" | "screen"; + +interface ApplicationViewabilityShow { + /** + * Show on media type. + * If used, `hide` cannot be set. + */ + show: media; + hide?: never; +} + +interface ApplicationViewabilityHide { + /** + * Hide on media type. + * If used, `show` cannot be set. + */ + hide: media; + show?: never; +} + +interface ApplicationViewabilityUndecided { + /** + * Only one child allowed. + * Need to process the `className` property. + */ + children: React.ReactElement<{ className?: string }>; +} + +export type ApplicationViewabilityProps = ApplicationViewabilityUndecided & + (ApplicationViewabilityShow | ApplicationViewabilityHide); + +/** + * Sets the viewability of the the contained element regarding media. + * Can be used to hide elements, e.g. when the page is printed. + */ +export const ApplicationViewability = ({ children, show, hide }: ApplicationViewabilityProps) => { + if (!show && !hide) { + return children; + } + if (show === hide) { + // eslint-disable-next-line no-console + console.warn("`` used with same media type for `hide` and `show`."); + return children; + } + + const enhancedClone = React.cloneElement(children, { + className: classNames(children.props.className, { + [`${eccgui}-application__hide--${hide}`]: hide, + [`${eccgui}-application__show--${show}`]: show, + }), + }); + + return enhancedClone; +}; + +export default ApplicationViewability; diff --git a/src/components/Application/_content.scss b/src/components/Application/_content.scss index d4ac1cc66..9cef44271 100644 --- a/src/components/Application/_content.scss +++ b/src/components/Application/_content.scss @@ -25,3 +25,10 @@ $ui-02: $eccgui-color-workspace-background !default; .#{$eccgui}-application__content--railsidebar { margin-left: mini-units(8); } + +@media print { + .#{$eccgui}-application__content { + padding: $eccgui-size-block-whitespace 0 0 0 !important; + margin: 0; + } +} diff --git a/src/components/Application/_header.scss b/src/components/Application/_header.scss index 59ec6b7fa..27d7fd492 100644 --- a/src/components/Application/_header.scss +++ b/src/components/Application/_header.scss @@ -98,10 +98,10 @@ span.#{$prefix}--header__name { .#{$eccgui}-application__title--content { display: inline-block; overflow: hidden; + text-overflow: ellipsis; font-size: $eccgui-size-typo-caption; font-weight: $eccgui-font-weight-bold; line-height: $eccgui-size-typo-caption-lineheight; - text-overflow: ellipsis; letter-spacing: $eccgui-font-spacing-wide; white-space: nowrap; } @@ -122,7 +122,7 @@ span.#{$prefix}--header__name { height: auto; max-height: mini-units(5); padding: 0; - margin: mini-units(1.4) 0 mini-units(1.6) 0; + margin: mini-units(1.4) 0 mini-units(1.6); vertical-align: middle; } } @@ -195,9 +195,9 @@ a.#{$prefix}--header__menu-item:active { .#{$prefix}--header__action.#{$prefix}--btn--primary:focus, a.#{$prefix}--header__name:focus, a.#{$prefix}--header__menu-item:focus { - border: none; outline: 1px dotted $shell-header-focus; outline-offset: -1px; + border: none; box-shadow: none; } .#{$prefix}--header__menu-title[aria-expanded="true"] { @@ -267,3 +267,12 @@ a.#{$prefix}--header__menu-item:focus > svg { margin: 0; } } + +@media print { + .#{$eccgui}-application__header { + position: relative; + & > :not(.#{$eccgui}-workspace__header) { + display: none; + } + } +} diff --git a/src/components/Application/_viewability.scss b/src/components/Application/_viewability.scss new file mode 100644 index 000000000..d944eba59 --- /dev/null +++ b/src/components/Application/_viewability.scss @@ -0,0 +1,13 @@ +@media print { + .#{eccgui}-application__hide--print, + .#{eccgui}-application__show--screen { + display: none !important; + } +} + +@media screen { + .#{eccgui}-application__hide--screen, + .#{eccgui}-application__show--print { + display: none !important; + } +} diff --git a/src/components/Application/application.scss b/src/components/Application/application.scss index ea0de1677..460d62ee2 100644 --- a/src/components/Application/application.scss +++ b/src/components/Application/application.scss @@ -10,3 +10,4 @@ // @import '~@carbon/react/scss/components/ui-shell/navigation-menu'; @import "content"; @import "dropzone"; +@import "viewability"; diff --git a/src/components/Application/index.ts b/src/components/Application/index.ts index eaa115f36..83cae7309 100644 --- a/src/components/Application/index.ts +++ b/src/components/Application/index.ts @@ -8,4 +8,5 @@ export * from "./ApplicationToolbar"; export * from "./ApplicationToolbarSection"; export * from "./ApplicationToolbarAction"; export * from "./ApplicationToolbarPanel"; +export * from "./ApplicationViewability"; export * from "./helper"; diff --git a/src/components/Application/stories/ApplicationViewability.stories.tsx b/src/components/Application/stories/ApplicationViewability.stories.tsx new file mode 100644 index 000000000..3290ee89d --- /dev/null +++ b/src/components/Application/stories/ApplicationViewability.stories.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { LoremIpsum } from "react-lorem-ipsum"; +import { Meta, StoryFn } from "@storybook/react"; + +import { ApplicationViewability } from "../../../index"; +export default { + title: "Components/Application/Viewability", + component: ApplicationViewability, + argTypes: { + children: { + control: false, + }, + hide: { + control: { + type: "radio", + }, + options: ["print", "screen"], + }, + show: { + control: { + type: "radio", + }, + options: ["print", "screen"], + }, + }, +} as Meta; + +const TemplateBasicExample: StoryFn = (args) => ; + +export const Default = TemplateBasicExample.bind({}); +Default.args = { + children: ( +
+ +
+ ), +}; diff --git a/src/components/Application/tests/ApplicationViewability.test.tsx b/src/components/Application/tests/ApplicationViewability.test.tsx new file mode 100644 index 000000000..16fa55645 --- /dev/null +++ b/src/components/Application/tests/ApplicationViewability.test.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { expect } from "@storybook/test"; +import { render } from "@testing-library/react"; + +import "@testing-library/jest-dom"; + +import { ApplicationViewability, ApplicationViewabilityProps, CLASSPREFIX as eccgui } from "../../../index"; + +import { Default as ApplicationViewabilityStory } from "./../stories/ApplicationViewability.stories"; + +const applyViewabilityAndCheckClass = (props: Omit) => { + const { container } = render(); + const element = container.getElementsByClassName( + props.hide ? `${eccgui}-application__hide--${props.hide}` : `${eccgui}-application__show--${props.show}` + ); + expect(element.length).toBe(1); + return element; +}; + +describe("ApplicationViewability", () => { + it("should be visible on `show=screen`", () => { + applyViewabilityAndCheckClass({ show: "screen" }); + /** + * Currently we cannot really test visibility via jest if it is defined by S/CSS rules because those styles are not known. + * Looks like it is not too easy to include and test them. + * So we only test for the correct CSS class. + */ + // console.log(window.getComputedStyle(element.item(0)??new Element).getPropertyValue("display")); + // waitFor(() => expect(element).toBeVisible()); + }); + it("should not be visible on `hide=screen`", () => { + applyViewabilityAndCheckClass({ hide: "screen" }); + // waitFor(() => expect(element).not.toBeVisible()); + }); + it("should be visible on `hide=print`", () => { + applyViewabilityAndCheckClass({ hide: "print" }); + // waitFor(() => expect(element).toBeVisible()); + }); + it("should not be visible on `show=print`", () => { + applyViewabilityAndCheckClass({ show: "print" }); + // waitFor(() => expect(element).not.toBeVisible()); + }); +}); diff --git a/src/components/Card/card.scss b/src/components/Card/card.scss index a551cbcf8..6f5797e64 100644 --- a/src/components/Card/card.scss +++ b/src/components/Card/card.scss @@ -288,3 +288,9 @@ $eccgui-size-card-spacing: $eccgui-size-typo-base !default; } } } + +@media print { + .#{$eccgui}-card__actions { + display: none; + } +} diff --git a/src/components/Checkbox/checkbox.scss b/src/components/Checkbox/checkbox.scss index f72a9fbdd..e575dc988 100644 --- a/src/components/Checkbox/checkbox.scss +++ b/src/components/Checkbox/checkbox.scss @@ -24,9 +24,15 @@ $control-indicator-spacing: $eccgui-size-inline-whitespace !default; // $switch-background-color-active: rgba($gray1, 0.5) !default; // $switch-background-color-disabled: $button-background-color-disabled !default; $switch-checked-background-color: $eccgui-color-accent !default; -$switch-checked-background-color-hover: eccgui-color-rgba($switch-checked-background-color, $eccgui-opacity-narrow) !default; +$switch-checked-background-color-hover: eccgui-color-rgba( + $switch-checked-background-color, + $eccgui-opacity-narrow +) !default; $switch-checked-background-color-active: $switch-checked-background-color-hover !default; -$switch-checked-background-color-disabled: eccgui-color-rgba($switch-checked-background-color, $eccgui-opacity-disabled) !default; +$switch-checked-background-color-disabled: eccgui-color-rgba( + $switch-checked-background-color, + $eccgui-opacity-disabled +) !default; @import "~@blueprintjs/core/src/components/forms/controls"; // Checkbox, Radio, Switch @@ -73,3 +79,9 @@ $switch-checked-background-color-disabled: eccgui-color-rgba($switch-checked-bac display: inline-block; vertical-align: text-top; } + +@media print { + .#{$ns}-control { + print-color-adjust: exact; + } +} diff --git a/src/components/ContentGroup/_contentgroup.scss b/src/components/ContentGroup/_contentgroup.scss index 4519fe65d..c5afaf5a6 100644 --- a/src/components/ContentGroup/_contentgroup.scss +++ b/src/components/ContentGroup/_contentgroup.scss @@ -60,3 +60,12 @@ $eccgui-color-scontentgroup-border-sub: eccgui-color-rgba( flex-shrink: 1; width: 100%; } + +@media print { + .#{$eccgui}-contentgroup__header__options { + display: none; + } + .#{$eccgui}-contentgroup--border-sub::after { + print-color-adjust: exact; + } +} diff --git a/src/components/Depiction/depiction.scss b/src/components/Depiction/depiction.scss index 44d6cda8b..a7a3dc9f0 100644 --- a/src/components/Depiction/depiction.scss +++ b/src/components/Depiction/depiction.scss @@ -220,3 +220,9 @@ $eccgui-size-depiction-border-radius: $pt-border-radius !default; position: fixed; left: -5000rem; } + +@media print { + .#{$eccgui}-depiction { + print-color-adjust: exact; + } +} diff --git a/src/components/FlexibleLayout/flexiblelayout.scss b/src/components/FlexibleLayout/flexiblelayout.scss index 2efeba7af..5cb8eb00f 100644 --- a/src/components/FlexibleLayout/flexiblelayout.scss +++ b/src/components/FlexibleLayout/flexiblelayout.scss @@ -46,3 +46,19 @@ flex-basis: auto; } } + +@media print { + .#{$eccgui}-flexible__container, + .#{$eccgui}-flexible__item { + position: relative; + display: block; + width: auto; + height: auto; + padding: 0; + margin: 0; + + &:is(.#{$eccgui}-flexible__item) { + margin-bottom: $eccgui-size-block-whitespace; + } + } +} diff --git a/src/components/Grid/grid.scss b/src/components/Grid/grid.scss index c3b5e55e5..4a4b72ba9 100644 --- a/src/components/Grid/grid.scss +++ b/src/components/Grid/grid.scss @@ -86,3 +86,20 @@ $grid-gutter: rem($eccgui-size-grid-gutter); flex-wrap: nowrap; } } + +@media print { + .#{$eccgui}-grid, + .#{$eccgui}-grid__row, + .#{$eccgui}-grid__column { + position: relative; + display: block; + width: auto; + height: auto; + padding: 0; + margin: 0; + + &:is(.#{$eccgui}-grid__column) { + margin-bottom: $eccgui-size-block-whitespace; + } + } +} diff --git a/src/components/Grid/stories/Grid.stories.tsx b/src/components/Grid/stories/Grid.stories.tsx index 0cd3c527b..96bf087e0 100644 --- a/src/components/Grid/stories/Grid.stories.tsx +++ b/src/components/Grid/stories/Grid.stories.tsx @@ -11,19 +11,22 @@ export default { subcomponents: { GridRow, GridColumn }, argTypes: { children: { - control: "none", + control: false, }, }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], } as Meta; -const Template: StoryFn = (args) => ( -
- -
-); +const Template: StoryFn = (args) => ; export const Default = Template.bind({}); Default.args = { - children: , + children: [, ], verticalStretchable: true, }; diff --git a/src/components/Grid/stories/GridRow.stories.tsx b/src/components/Grid/stories/GridRow.stories.tsx index e126170ab..28ed25677 100644 --- a/src/components/Grid/stories/GridRow.stories.tsx +++ b/src/components/Grid/stories/GridRow.stories.tsx @@ -10,21 +10,27 @@ export default { component: GridRow, argTypes: { children: { - control: "none", + control: false, }, }, } as Meta; -const Template: StoryFn = (args) => ( - - - -); +const Template: StoryFn = (args) => ; export const Default = Template.bind({}); Default.args = { - children: , + children: [ + , + , + ], }; +Default.decorators = [ + (Story) => ( + + + + ), +]; const TemplateStretched: StoryFn = (args) => ( diff --git a/src/components/Notification/notification.scss b/src/components/Notification/notification.scss index 8df9a59b0..ac376c280 100644 --- a/src/components/Notification/notification.scss +++ b/src/components/Notification/notification.scss @@ -91,3 +91,9 @@ color: inherit; } } + +@media print { + .#{$eccgui}-notification__actions { + display: none; + } +} diff --git a/src/components/OverviewItem/overviewitem.scss b/src/components/OverviewItem/overviewitem.scss index 41744f93f..4381d429b 100644 --- a/src/components/OverviewItem/overviewitem.scss +++ b/src/components/OverviewItem/overviewitem.scss @@ -197,3 +197,12 @@ $eccgui-size-overviewitem-line-typo-large-lineheight: $eccgui-size-typo-subtitle display: flex; } } + +@media print { + .#{$eccgui}-overviewitem__actions { + display: none; + } + .#{$eccgui}-overviewitem__depiction { + print-color-adjust: exact; + } +} diff --git a/src/components/PropertyValuePair/propertyvalue.scss b/src/components/PropertyValuePair/propertyvalue.scss index 494f6ace0..1f1edc292 100644 --- a/src/components/PropertyValuePair/propertyvalue.scss +++ b/src/components/PropertyValuePair/propertyvalue.scss @@ -5,9 +5,9 @@ } .#{$eccgui}-propertyvalue__pair { + clear: both; display: block; width: 100%; - clear: both; &.#{$eccgui}-propertyvalue__pair--hasdivider { &:not(:last-child) { @@ -94,3 +94,25 @@ } } } + +@media print { + .#{$eccgui}-propertyvalue__pair, + .#{$eccgui}-propertyvalue__property, + .#{$eccgui}-propertyvalue__value { + position: relative; + float: none !important; + display: block; + width: auto; + height: auto; + min-height: 0 !important; + padding: 0; + margin: 0 !important; + + &:is(.#{$eccgui}-propertyvalue__property) { + margin-bottom: 0.25 * $eccgui-size-block-whitespace !important; + } + &:is(.#{$eccgui}-propertyvalue__pair) { + margin-bottom: 0.5 * $eccgui-size-block-whitespace !important; + } + } +} diff --git a/src/components/Separation/separation.scss b/src/components/Separation/separation.scss index 239b8f101..dc32d7480 100644 --- a/src/components/Separation/separation.scss +++ b/src/components/Separation/separation.scss @@ -101,3 +101,9 @@ $eccgui-color-separation-divider: $pt-divider-black !default; margin: 0 $eccgui-size-separation-spacing-medium; } } + +@media print { + .#{$eccgui}-separation__divider-horizontal { + print-color-adjust: exact; + } +} diff --git a/src/components/Table/table.scss b/src/components/Table/table.scss index 1f9bd5223..e926e8619 100644 --- a/src/components/Table/table.scss +++ b/src/components/Table/table.scss @@ -356,3 +356,25 @@ tr.#{$prefix}--parent-row.#{$prefix}--expandable-row + tr[data-child-row] { min-height: $eccgui-size-tablecell-height-regular; } } + +@media print { + .#{$eccgui}-simpletable:has(.#{$eccgui}-simpletable__cell > .#{$eccgui}-typography__overflowtext) { + // allow 2 lines of text in `` elements that are direct children of table cells + .#{$eccgui}-simpletable__cell { + & > .#{$eccgui}-typography__overflowtext, + & > .#{$eccgui}-typography__overflowtext--passdown { + display: inline; + overflow: visible; + text-overflow: unset; + white-space: unset; + } + & > .#{$eccgui}-typography__overflowtext { + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + } + } + } +} diff --git a/src/components/Tag/tag.scss b/src/components/Tag/tag.scss index 2ef47acdb..49e3dd222 100644 --- a/src/components/Tag/tag.scss +++ b/src/components/Tag/tag.scss @@ -267,3 +267,9 @@ $tag-round-adjustment: 0 !default; } } } + +@media print { + .#{$eccgui}-tag__item { + print-color-adjust: exact; + } +}