From 478591950051d60821e01a19a528b95e2fff2192 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 28 Jul 2023 12:55:22 +0200 Subject: [PATCH] chore: Header responsiveness explorations --- .github/workflows/build-lint-test.yml | 2 +- pages/header/responsiveness-2.page.tsx | 36 ++++ pages/header/responsiveness.page.tsx | 288 +++++++++++++++++++++++++ pages/header/styles.scss | 198 +++++++++++++++++ src/header/internal.tsx | 28 ++- src/header/styles.scss | 81 ++++--- 6 files changed, 589 insertions(+), 44 deletions(-) create mode 100644 pages/header/responsiveness-2.page.tsx create mode 100644 pages/header/responsiveness.page.tsx create mode 100644 pages/header/styles.scss diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index 7234942c4a..28ab8326d2 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -20,7 +20,7 @@ permissions: jobs: build: - uses: cloudscape-design/.github/.github/workflows/build-lint-test.yml@main + uses: cloudscape-design/.github/.github/workflows/build-lint-test.yml@gasda/quick-deploy secrets: inherit with: artifact-path: lib/static-default diff --git a/pages/header/responsiveness-2.page.tsx b/pages/header/responsiveness-2.page.tsx new file mode 100644 index 0000000000..6f2ffef857 --- /dev/null +++ b/pages/header/responsiveness-2.page.tsx @@ -0,0 +1,36 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +/* eslint-disable react/jsx-key */ +import React, { useState } from 'react'; +import clsx from 'clsx'; +import Box from '~components/box'; +import Input from '~components/input'; +import FormField from '~components/form-field'; +import ScreenshotArea from '../utils/screenshot-area'; +import styles from './styles.scss'; +import { Showcase } from './responsiveness.page'; + +export default function PageHeadersDemo() { + const [value, setValue] = useState('75'); + return ( + + + +
+ { + setValue(e.detail.value); + document.documentElement.style.setProperty('--header-max-width', `${e.detail.value}ch`); + }} + /> +
+
+
+ +
+
+
+ ); +} diff --git a/pages/header/responsiveness.page.tsx b/pages/header/responsiveness.page.tsx new file mode 100644 index 0000000000..c8b74c120a --- /dev/null +++ b/pages/header/responsiveness.page.tsx @@ -0,0 +1,288 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +/* eslint-disable react/jsx-key */ +import React, { useRef, useState } from 'react'; +import clsx from 'clsx'; +import { useResizeObserver } from '@cloudscape-design/component-toolkit/internal'; +import Box from '~components/box'; +import Button from '~components/button'; +import ButtonDropdown from '~components/button-dropdown'; +import Container from '~components/container'; +import Header, { HeaderProps } from '~components/header'; +import Link from '~components/link'; +import SpaceBetween from '~components/space-between'; +import ScreenshotArea from '../utils/screenshot-area'; +import styles from './styles.scss'; +import { Toggle } from '~components'; + +type Approach = 'current' | 'grid' | 'flex' | 'cquery' | 'resize-observer' | 'scott'; + +type DemoHeaderProps = Pick; + +function ToggleButton() { + const [status, setStatus] = useState(false); + return ; +} + +const permutations: Array> = [ + { + children: 'Simple container header', + description: ( + + Fairly short description for this container. Not much to see here except for{' '} + + a link + + . + + ), + actions: , + }, + { + children: 'Tags (4)', + info: Info, + description: + 'A tag is a label that you assign to an AWS resource. Each tag consists of a key and an optional value. You can use tags to search and filter your resources or track your AWS costs.', + actions: , + }, + { + children: 'Snapshots', + description: 'Select at least one snapshot to perform an action.', + actions: ( + + + + ), + }, + { + children: 'Workgroups', + description: + 'Use workgroups to separate users, teams, applications, workloads, and to set limits on amount of data for each query or the entire workgroup process. You can also view query-related metrics in AWS CloudWatch.', + actions: ( + + Actions + + + ), + }, + { + children: 'Access Points (100)', + info: Info, + description: ( + + Amazon S3 Access Points simplify managing data access at scale for shared datasets in S3. Access points are + named network endpoints that are attached to buckets that you can use to perform S3 object operations. An Access + Point alias provides the same functionality as an Access Point ARN and can be substituted for use anywhere an S3 + bucket name is normally used for data access.{' '} + + Learn more + + + ), + actions: ( + + + + + + + + ), + }, + { + children: 'Capacity reservations for the US East (South America São Paulo) Region', + info: Info, + description: ( + + Amazon S3 Access Points simplify managing data access at scale for shared datasets in S3. Access points are + named network endpoints that are attached to buckets that you can use to perform S3 object operations. + + ), + actions: ( + + + + + ), + }, +]; + +function HeaderGrid({ children, actions, description }: DemoHeaderProps) { + return ( +
+ + {children} + + + {description} + +
{actions}
+
+ ); +} + +function HeaderFlex({ children, actions, description }: DemoHeaderProps) { + return ( +
+
+ + {children} + + + {description} + +
+
{actions}
+
+ ); +} + +function HeaderContainerQuery({ children, actions, description }: DemoHeaderProps) { + return ( +
+
+ + {children} + + + {description} + +
{actions}
+
+
+ ); +} + +function HeaderScott({ children, actions, description }: DemoHeaderProps) { + return ( +
+
+ + {children} + + + {description} + +
{actions}
+
+
+ ); +} + +function HeaderResizeObserver({ children, actions, description }: DemoHeaderProps) { + const containerRef = useRef(null); + const actionsRef = useRef(null); + const [wrapActions, setWrapActions] = useState(false); + + const updateLayout = () => { + if (!containerRef.current || !actionsRef.current) { + return; + } + const container = containerRef.current; + const actions = actionsRef.current; + const title = container.querySelector(`.${styles['header-grid--title']}`)!; + + // const oldRect = actions.getBoundingClientRect(); + actions.style.width = 'max-content'; + const intrinsicRect = actions.getBoundingClientRect(); + actions.style.width = ''; + + const containerRect = container.getBoundingClientRect(); + const titleRect = title.getBoundingClientRect(); + + // console.log({ containerWidth: containerRect.width, titleWidth: titleRect.width, actionsWidth: oldRect.width }); + + const distance = containerRect.width - (titleRect.width + intrinsicRect.width); + // console.log({ distance }); + setWrapActions(distance < 50); + }; + + useResizeObserver(containerRef, () => { + updateLayout(); + }); + useResizeObserver(actionsRef, () => { + updateLayout(); + }); + return ( +
+ + {children} + + + {description} + +
+ {actions} +
+
+ ); +} + +function renderHeader(approach: Approach, props: Partial) { + switch (approach) { + case 'grid': + return ; + case 'flex': + return ; + case 'cquery': + return ; + case 'resize-observer': + return ; + case 'scott': + return ; + case 'current': + default: + return
; + } +} + +const approachDescription: Record = { + current: 'Current implementation', + grid: 'CSS Grid that places actions in the top right', + flex: 'Similar to current approach but with different flows', + cquery: 'Like grid, but rearranges actions when container is small', + 'resize-observer': 'not working', + scott: '', +}; + +export function Showcase({ approach }: { approach: Approach }) { + return ( +
+
+

Approach: {approach}

+

{approachDescription[approach]}

+
+ + {permutations.map((props, i) => { + return ( + + Container content + + ); + })} +
+ ); +} + +export default function PageHeadersDemo() { + const [subgrid, setSubgrid] = useState(false); + return ( + + + setSubgrid(detail.checked)}> + Side by side + +
+ + + {/* */} + {/* */} + + {/* */} +
+
+
+ ); +} diff --git a/pages/header/styles.scss b/pages/header/styles.scss new file mode 100644 index 0000000000..91e1e82ffa --- /dev/null +++ b/pages/header/styles.scss @@ -0,0 +1,198 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +/* stylelint-disable */ + +@use '~design-tokens' as awsui; + +.playground { + display: grid; + // grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); + grid-template-columns: repeat(1, 1fr); + column-gap: awsui.$space-scaled-l; + + &.playground--side-by-side { + grid-template-columns: repeat(2, 1fr); + + .showcase:nth-child(1) { + grid-column: span 2; + > * { + width: 50%; + grid-column: span 2; + } + } + } +} + +.showcase { + display: grid; + + grid-template-columns: 1fr; + grid-template-rows: auto; + grid-row: span 6; + row-gap: awsui.$space-scaled-s; + // grid-row: 1 / 100; + + grid-template-columns: subgrid; + grid-template-rows: subgrid; +} + +///// AUTOFLOW APPROACH (doesnt work well) +// .header-grid { +// display: grid; +// grid-auto-flow: dense; +// grid-auto-columns: 1fr minmax(auto, 75%); +// column-gap: awsui.$space-scaled-xs; +// row-gap: awsui.$space-scaled-xs; + +// > .header-grid--description { +// grid-column-end: span 2; +// } +// } + +////// GRID AREAS APPROACH +.header-grid { + display: grid; + grid-template-areas: + 'title actions' + 'description description'; + grid-template-columns: max-content 1fr; + column-gap: awsui.$space-scaled-xs; + row-gap: awsui.$space-scaled-xs; + + > .header-grid--title { + grid-area: title; + } + > .header-grid--description { + grid-area: description; + } + > .header-grid--actions { + grid-area: actions; + justify-self: flex-end; + } + &.header-grid--overlap { + // For the resize observer approach + grid-template-areas: + 'title' + 'description' + 'actions'; + // display: block; + grid-template-columns: 1fr; + + > .header-grid--title { + width: fit-content; + } + > .header-cquery--actions, + > .header-grid--actions { + justify-self: flex-start; + } + } +} + +////// FLEXBOX APPROACH +.header-flex { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + column-gap: awsui.$space-scaled-xs; + + container-type: inline-size; +} +.header-flex--heading { + display: flex; + flex-direction: column; + flex-basis: 60%; + flex-grow: 1; + flex-shrink: 1; +} +.header-flex--actions { + display: flex; + align-items: flex-start; +} +@container (max-width: 760px) { + .header-flex--heading { + flex-basis: auto; + } +} + +////// CONTAINER QUERY APPROACH +.header-cquery-wrapper { + container-type: inline-size; +} +.header-cquery { + display: grid; + grid-template-areas: + 'title actions' + 'description description'; + grid-template-columns: 1fr auto; + column-gap: awsui.$space-scaled-xs; + row-gap: awsui.$space-scaled-xs; + + > .header-cquery--title { + grid-area: title; + } + > .header-cquery--description { + grid-area: description; + } + > .header-cquery--actions { + grid-area: actions; + justify-self: flex-end; + } +} +@container (max-width: 900px) { + .header-cquery { + grid-template-areas: + 'title' + 'description' + 'actions'; + + > .header-cquery--actions { + justify-self: flex-start; + } + } +} + +.scott { + container: test / inline-size; +} +.scott-container { + column-gap: awsui.$space-scaled-m; + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(4, auto); +} +.scott-title { + grid-column: 1 / 13; + grid-row: 1; +} +.scott-actions { + display: flex; + grid-column: 1 / 13; + grid-row: 3; +} +.scott-description { + grid-column: 1 / 13; + grid-row: 2; + margin: awsui.$space-scaled-m 0; +} +@container test (min-width: 651px) { + .scott-title { + grid-column: 1 / 7; + grid-row: 1 / 3; + } + .scott-actions { + justify-content: end; + grid-column: 7 / 13; + grid-row: 1; + } + .scott-description { + grid-column: 1 / 11; + grid-row: 3; + // margin: awsui.$space-scaled-m 0; + } +} + +/* stylelint-enable */ diff --git a/src/header/internal.tsx b/src/header/internal.tsx index 76097d5de0..9edc787dc3 100644 --- a/src/header/internal.tsx +++ b/src/header/internal.tsx @@ -54,12 +54,18 @@ export default function InternalHeader({ styles[`root-variant-${variantOverride}`], isRefresh && styles.refresh, !actions && [styles[`root-no-actions`]], - description && [styles[`root-has-description`]], - __disableActionsWrapping && [styles['root-no-wrap']] + description && [styles[`root-has-description`]] )} ref={__internalRootRef} > -
+
{info}}
- {description} + {actions && ( +
+ {actions} +
+ )}
- {actions && ( -
- {actions} -
- )} + {description}
); } diff --git a/src/header/styles.scss b/src/header/styles.scss index 38abe6331e..bae0910ecf 100644 --- a/src/header/styles.scss +++ b/src/header/styles.scss @@ -17,9 +17,10 @@ width: 100%; flex-wrap: wrap; justify-content: space-between; - column-gap: awsui.$space-xs; - &.refresh { - row-gap: awsui.$space-scaled-xxs; + + &.refresh, + &:not(.root-no-actions) { + row-gap: awsui.$space-xxs; } &-no-actions, @@ -29,14 +30,26 @@ padding-bottom: awsui.$space-scaled-xxs; } } + + &-no-actions.refresh { + &.root-variant-h2, + &.root-variant-h3 { + row-gap: awsui.$space-scaled-xxxs; + } + } + + &.root-variant-h1:not(.refresh) { + row-gap: awsui.$space-scaled-xxs; + } + // H3s are most often used inside containers, so minimal headings have extra padding below // to prevent info links from having overlapping touch targets with surrounding elements. &-no-actions:not(.root-has-description).refresh.root-variant-h3 { padding-bottom: awsui.$space-scaled-xs; } - &-no-wrap { - flex-wrap: nowrap; + &.root-variant-h1.root-has-description:not(.refresh) { + padding-bottom: awsui.$space-scaled-2x-xxs; } } @@ -44,34 +57,27 @@ // Like styles.text-flex-wrapping, but without overflow: hidden, to prevent headings from being partially cut off. word-wrap: break-word; max-width: 100%; + display: flex; + justify-content: space-between; + width: 100%; + flex-wrap: wrap; + + &.no-wrap { + flex-wrap: nowrap; + } &.refresh { - display: flex; - flex-direction: column; // Can't use justify-content: center because it won't align with configurable dashboard fixed handle icon + row-gap: awsui.$space-scaled-xxs; } &-variant-h1 { - padding: awsui.$space-scaled-2x-xxs 0; - &.refresh { - padding: 0; - row-gap: awsui.$space-scaled-xxs; - } + row-gap: awsui.$space-scaled-2x-xxs; } +} - // So that headers with only a title have the same height as headers with actions and descriptions - &-variant-h2, - &-variant-h3 { - &:not(.refresh) { - padding-top: awsui.$space-scaled-xxs; - :not(.root-has-description) > & { - padding-bottom: awsui.$space-xxxs; - } - } - &.refresh { - row-gap: awsui.$space-scaled-xxxs; - } - } +.root-has-description > .main { + row-gap: awsui.$space-scaled-2x-xxs; } .actions { @@ -98,9 +104,10 @@ &-variant-h2:not(.refresh), &-variant-h3:not(.refresh) { padding: awsui.$space-scaled-xxs 0; - .root-has-description > & { - padding-bottom: 0; - } + } + + .root-has-description > .main > & { + padding-bottom: 0; } } @@ -110,6 +117,7 @@ &-variant-h1 { font-size: awsui.$font-size-heading-xl; + padding-top: awsui.$space-scaled-2x-xxs; // Use padding rather than center align with min height to avoid having extra bottom space when no actions are present. // Having top padding always present ensures that all headers of the same variant start at the same height in the container, // whether there are buttons present or not; otherwise configurable dashboard handles are misaligned. @@ -131,10 +139,22 @@ } &-variant-h2:not(.refresh), &-variant-h3:not(.refresh) { - padding: awsui.$space-scaled-xxs 0; + padding-top: awsui.$space-scaled-xs; + padding-bottom: calc(#{awsui.$space-scaled-xxs} + #{awsui.$space-scaled-xxxs}); + } +} + +.root-no-actions.root-has-description > .main > :not(.refresh) { + &.title-variant-h2, + &.title-variant-h3 { + padding-bottom: awsui.$space-scaled-xxs; } } +.root-variant-h1.root-no-actions:not(.root-has-description):not(.refresh) { + padding-bottom: awsui.$space-scaled-2x-xxs; +} + .info { // Space 's' used as it's the smallest value that works in all browsers padding-right: awsui.$space-s; @@ -148,9 +168,6 @@ &-variant-h1 { @include styles.font-body-m; - &:not(.refresh) { - padding-top: awsui.$space-scaled-xxs; - } } &-variant-h2 { font-size: awsui.$font-header-h2-description-size;