Skip to content

Commit

Permalink
feat: flex-wrap fallback component
Browse files Browse the repository at this point in the history
  • Loading branch information
Robbert committed Oct 16, 2023
1 parent 806387d commit a8c0ba8
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 0 deletions.
9 changes: 9 additions & 0 deletions components/flex-wrap-fallback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- @license CC0-1.0 -->

# 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.
5 changes: 5 additions & 0 deletions components/flex-wrap-fallback/tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"utrecht": {
"flex-wrap-fallback": {}
}
}
155 changes: 155 additions & 0 deletions packages/storybook-web-component/src/FlexWrapFallback.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<FlexWrapFallbackStoryProps>) => (
<UtrechtFlexWrapFallback {...restProps}>
{children}
{fallback}
</UtrechtFlexWrapFallback>
);

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<typeof FlexWrapFallbackStory>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
children: [
<div
style={{
display: 'flex',
flexWrap: 'wrap',
minHeight: '100px',
justifyContent: 'space-between',
inlineSize: '100%',
}}
>
<div>
<a href="https://example.com/">Home</a>
</div>
<div>
<a href="https://example.com/2">Link 2</a>
</div>
<div>
<a href="https://example.com/3">Link 3</a>
</div>
<div>
<a href="https://example.com/4">Link 4</a>
</div>
<div>
<a href="https://example.com/5">Link 5</a>
</div>
<div>
<a href="https://example.com/6">Link 6</a>
</div>
<div>
<a href="https://example.com/7">Link 7</a>
</div>
<div>
<a href="https://example.com/8">Link 8</a>
</div>
<div>
<a href="https://example.com/9">Link 9</a>
</div>
</div>,
],
fallback: [
<UtrechtButtonGroup slot="fallback">
<UtrechtButton appearance="subtle" expanded="false">
Open menu
</UtrechtButton>
</UtrechtButtonGroup>,
<UtrechtDrawer open slot="fallback">
<ul>
<li>
<UtrechtLink href="https://example.com/">Home</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/2">Link 2</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/3">Link 3</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/4">Link 4</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/5">Link 5</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/6">Link 6</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/7">Link 7</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/8">Link 8</UtrechtLink>
</li>
<li>
<UtrechtLink href="https://example.com/9">Link 9</UtrechtLink>
</li>
</ul>
</UtrechtDrawer>,
],
},
decorators: [(Story) => <div style={{ inlineSize: '1280px', minBlockSize: '240px' }}>{Story()}</div>],
name: 'No flex-wrap (1280px wide)',
};

export const SmallInlineSize: Story = {
args: {
...Default.args,
},
decorators: [(Story) => <div style={{ maxInlineSize: '320px', minBlockSize: '240px' }}>{Story()}</div>],
name: 'Flex-wrap fallback (320px wide)',
};

export const DesignTokens = designTokenStory(meta);
13 changes: 13 additions & 0 deletions packages/web-component-library-stencil/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,8 @@ export namespace Components {
}
interface UtrechtUrlData {
}
interface UtrechtWrapAlternative {
}
}
export interface UtrechtButtonCustomEvent<T> extends CustomEvent<T> {
detail: T;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -2773,6 +2781,7 @@ declare global {
"utrecht-textarea": HTMLUtrechtTextareaElement;
"utrecht-textbox": HTMLUtrechtTextboxElement;
"utrecht-url-data": HTMLUtrechtUrlDataElement;
"utrecht-wrap-alternative": HTMLUtrechtWrapAlternativeElement;
}
}
declare namespace LocalJSX {
Expand Down Expand Up @@ -3518,6 +3527,8 @@ declare namespace LocalJSX {
}
interface UtrechtUrlData {
}
interface UtrechtWrapAlternative {
}
interface IntrinsicElements {
"utrecht-alert": UtrechtAlert;
"utrecht-article": UtrechtArticle;
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -4113,6 +4125,7 @@ declare module "@stencil/core" {
"utrecht-textarea": LocalJSX.UtrechtTextarea & JSXBase.HTMLAttributes<HTMLUtrechtTextareaElement>;
"utrecht-textbox": LocalJSX.UtrechtTextbox & JSXBase.HTMLAttributes<HTMLUtrechtTextboxElement>;
"utrecht-url-data": LocalJSX.UtrechtUrlData & JSXBase.HTMLAttributes<HTMLUtrechtUrlDataElement>;
"utrecht-wrap-alternative": LocalJSX.UtrechtWrapAlternative & JSXBase.HTMLAttributes<HTMLUtrechtWrapAlternativeElement>;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div
id="wrap"
class={`utrecht-flex-wrap-fallback__content ${
contentWraps ? 'utrecht-flex-wrap-fallback__content--hidden' : ''
}`}
data-inert={contentWraps}
aria-hidden={contentWraps}
>
<slot></slot>
</div>
<div
id="nowrap"
class={`utrecht-flex-wrap-fallback__fallback ${
contentWraps ? '' : 'utrecht-flex-wrap-fallback__fallback--hidden'
}`}
data-inert={!contentWraps}
hidden={!contentWraps}
>
<slot name="fallback"></slot>
</div>
</div>
);
}
}
Loading

0 comments on commit a8c0ba8

Please sign in to comment.