diff --git a/components/flex-wrap-fallback/README.md b/components/flex-wrap-fallback/README.md new file mode 100644 index 00000000000..10d4a5339c6 --- /dev/null +++ b/components/flex-wrap-fallback/README.md @@ -0,0 +1,9 @@ + + +# Flex wrap fallback + +The _flex wrap fallback_ component will choose between the default slot, and a fallback slot, based on flexbox wrapping. When the default content wraps over multiple lines, it will display the fallback content instead. + +For example: you can choose between a menu bar navigation for wide screens, and a hamburger menu for narrow screens. + +**Warning:** this component is experimental. diff --git a/components/flex-wrap-fallback/tokens.json b/components/flex-wrap-fallback/tokens.json new file mode 100644 index 00000000000..c3a93aa13e7 --- /dev/null +++ b/components/flex-wrap-fallback/tokens.json @@ -0,0 +1,5 @@ +{ + "utrecht": { + "flex-wrap-fallback": {} + } +} diff --git a/packages/storybook-web-component/src/FlexWrapFallback.stories.tsx b/packages/storybook-web-component/src/FlexWrapFallback.stories.tsx new file mode 100644 index 00000000000..fe4181dcd91 --- /dev/null +++ b/packages/storybook-web-component/src/FlexWrapFallback.stories.tsx @@ -0,0 +1,155 @@ +/* @license CC0-1.0 */ + +import type { Meta, StoryObj } from '@storybook/react'; +import readme from '@utrecht/components/flex-wrap-fallback/README.md?raw'; +import tokensDefinition from '@utrecht/components/flex-wrap-fallback/tokens.json'; +import tokens from '@utrecht/design-tokens/dist/index.json'; +import { + UtrechtButton, + UtrechtButtonGroup, + UtrechtDrawer, + UtrechtFlexWrapFallback, + UtrechtLink, +} from '@utrecht/web-component-library-react'; +import React, { PropsWithChildren, ReactNode } from 'react'; +import { designTokenStory } from './design-token-story'; + +interface FlexWrapFallbackStoryProps { + fallback?: ReactNode; +} + +const FlexWrapFallbackStory = ({ children, fallback, ...restProps }: PropsWithChildren) => ( + + {children} + {fallback} + +); + +const meta = { + title: 'Web Component/Flex-wrap fallback', + id: 'web-component-flex-wrap-fallback', + component: FlexWrapFallbackStory, + argTypes: { + children: { + description: 'Content', + }, + fallback: { + description: 'Fallback content', + }, + }, + args: { + children: [], + }, + tags: ['autodocs'], + parameters: { + status: { + type: 'WORK IN PROGRESS', + }, + tokensPrefix: 'utrecht-flex-wrap-fallback', + tokens, + tokensDefinition, + docs: { + description: { + component: readme, + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + children: [ +
+
+ Home +
+
+ Link 2 +
+
+ Link 3 +
+
+ Link 4 +
+
+ Link 5 +
+
+ Link 6 +
+
+ Link 7 +
+
+ Link 8 +
+
+ Link 9 +
+
, + ], + fallback: [ + + + Open menu + + , + +
    +
  • + Home +
  • +
  • + Link 2 +
  • +
  • + Link 3 +
  • +
  • + Link 4 +
  • +
  • + Link 5 +
  • +
  • + Link 6 +
  • +
  • + Link 7 +
  • +
  • + Link 8 +
  • +
  • + Link 9 +
  • +
+
, + ], + }, + decorators: [(Story) =>
{Story()}
], + name: 'No flex-wrap (1280px wide)', +}; + +export const SmallInlineSize: Story = { + args: { + ...Default.args, + }, + decorators: [(Story) =>
{Story()}
], + name: 'Flex-wrap fallback (320px wide)', +}; + +export const DesignTokens = designTokenStory(meta); diff --git a/packages/web-component-library-stencil/src/components.d.ts b/packages/web-component-library-stencil/src/components.d.ts index 34b32c0315d..fd30ad039a3 100644 --- a/packages/web-component-library-stencil/src/components.d.ts +++ b/packages/web-component-library-stencil/src/components.d.ts @@ -720,6 +720,8 @@ export namespace Components { } interface UtrechtUrlData { } + interface UtrechtWrapAlternative { + } } export interface UtrechtButtonCustomEvent extends CustomEvent { detail: T; @@ -2486,6 +2488,12 @@ declare global { prototype: HTMLUtrechtUrlDataElement; new (): HTMLUtrechtUrlDataElement; }; + interface HTMLUtrechtWrapAlternativeElement extends Components.UtrechtWrapAlternative, HTMLStencilElement { + } + var HTMLUtrechtWrapAlternativeElement: { + prototype: HTMLUtrechtWrapAlternativeElement; + new (): HTMLUtrechtWrapAlternativeElement; + }; interface HTMLElementTagNameMap { "utrecht-alert": HTMLUtrechtAlertElement; "utrecht-article": HTMLUtrechtArticleElement; @@ -2773,6 +2781,7 @@ declare global { "utrecht-textarea": HTMLUtrechtTextareaElement; "utrecht-textbox": HTMLUtrechtTextboxElement; "utrecht-url-data": HTMLUtrechtUrlDataElement; + "utrecht-wrap-alternative": HTMLUtrechtWrapAlternativeElement; } } declare namespace LocalJSX { @@ -3518,6 +3527,8 @@ declare namespace LocalJSX { } interface UtrechtUrlData { } + interface UtrechtWrapAlternative { + } interface IntrinsicElements { "utrecht-alert": UtrechtAlert; "utrecht-article": UtrechtArticle; @@ -3805,6 +3816,7 @@ declare namespace LocalJSX { "utrecht-textarea": UtrechtTextarea; "utrecht-textbox": UtrechtTextbox; "utrecht-url-data": UtrechtUrlData; + "utrecht-wrap-alternative": UtrechtWrapAlternative; } } export { LocalJSX as JSX }; @@ -4113,6 +4125,7 @@ declare module "@stencil/core" { "utrecht-textarea": LocalJSX.UtrechtTextarea & JSXBase.HTMLAttributes; "utrecht-textbox": LocalJSX.UtrechtTextbox & JSXBase.HTMLAttributes; "utrecht-url-data": LocalJSX.UtrechtUrlData & JSXBase.HTMLAttributes; + "utrecht-wrap-alternative": LocalJSX.UtrechtWrapAlternative & JSXBase.HTMLAttributes; } } } diff --git a/packages/web-component-library-stencil/src/components/flex-wrap-fallback.scss b/packages/web-component-library-stencil/src/components/flex-wrap-fallback.scss new file mode 100644 index 00000000000..9654ce98d1e --- /dev/null +++ b/packages/web-component-library-stencil/src/components/flex-wrap-fallback.scss @@ -0,0 +1,24 @@ +/** + * @license EUPL-1.2 + * Copyright (c) 2020-2022 Gemeente Utrecht + * Copyright (c) 2020-2022 Frameless B.V. + */ + +:host { + display: block; +} + +:host([hidden]) { + display: none !important; +} + +.utrecht-flex-wrap-fallback__content--hidden, +.utrecht-flex-wrap-fallback__fallback--hidden { + block-size: 0; + opacity: 0%; + outline: 0; + overflow: hidden; + pointer-events: none; + position: relative; + user-select: none; +} diff --git a/packages/web-component-library-stencil/src/components/flex-wrap-fallback.tsx b/packages/web-component-library-stencil/src/components/flex-wrap-fallback.tsx new file mode 100644 index 00000000000..23141c3fd29 --- /dev/null +++ b/packages/web-component-library-stencil/src/components/flex-wrap-fallback.tsx @@ -0,0 +1,83 @@ +/** + * @license EUPL-1.2 + * Copyright (c) 2020-2022 Gemeente Utrecht + * Copyright (c) 2020-2022 Frameless B.V. + */ + +import { Component, Element, h, Prop, State } from '@stencil/core'; + +/** + * Checks if an element has `flex-wrap: wrap` and the content wraps over multiple lines + */ +const hasFlexboxWrap = (el: Element) => { + const win = el?.ownerDocument?.defaultView; + + const style = win?.getComputedStyle(el); + + if (style && style.getPropertyValue('flex-wrap') === 'wrap' && style.getPropertyValue('display') === 'flex') { + // TODO: Account for writing-mode vertical + return el.firstElementChild.getBoundingClientRect().top !== el.lastElementChild.getBoundingClientRect().top; + } else { + return false; + } +}; + +@Component({ + tag: 'utrecht-flex-wrap-fallback', + styleUrl: 'flex-wrap-fallback.scss', + shadow: true, +}) +export class FlexWrapFallback { + @Element() hostElement: HTMLElement; + + /* ID reference for element with `display: flex;` or `display: inline-flex` and `flex-wrap: wrap` */ + @Prop({ attribute: 'flextarget', reflect: true }) flexTarget?: string; + @State() resizeObserver: ResizeObserver; + @State() contentWraps: boolean; + + connectedCallback() { + if (!this.resizeObserver) { + this.resizeObserver = new ResizeObserver(() => { + // TODO: Support updating `flextarget` attribute, switch ResizeObserver to match + // TODO: Escape ID selector CSS query + const cssSelector = this.flexTarget ? `#${this.flexTarget}` : ':not([slot])'; + const flexTargetEl = this.hostElement.querySelector(cssSelector); + console.log(cssSelector, flexTargetEl); + this.contentWraps = !!flexTargetEl && hasFlexboxWrap(flexTargetEl); + }); + } + + this.resizeObserver.observe(this.hostElement); + } + + disconnectedCallback() { + this.resizeObserver?.unobserve(this.hostElement); + } + render() { + const { contentWraps } = this; + return ( +
+
+ +
+ +
+ ); + } +} diff --git a/packages/web-component-library-stencil/src/index.html b/packages/web-component-library-stencil/src/index.html index 1caa0cb0640..548c7d063c6 100644 --- a/packages/web-component-library-stencil/src/index.html +++ b/packages/web-component-library-stencil/src/index.html @@ -18,6 +18,29 @@ + @@ -27,6 +50,35 @@ + + + + Open menu + + +
    +
  • Link 1
  • +
  • Link 2
  • +
  • Link 3
  • +
  • Link 4
  • +
  • Link 5
  • +
  • Link 6
  • +
  • Link 7
  • +
  • Link 8
  • +
  • Link 9
  • +
+
+