Skip to content

Commit

Permalink
feat(popover): improve docs
Browse files Browse the repository at this point in the history
  • Loading branch information
eTallang committed Jan 20, 2025
1 parent c365859 commit fe8ab44
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 0.0.9 - xx.01.2025

- Lagt til select-komponent
- Lagt til popover-komponent

# 0.0.8 - 13.01.2025

Expand Down
35 changes: 18 additions & 17 deletions packages/lib/components/popover/Overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,25 @@ import * as stories from "./popover.stories";

<Title />
<Subtitle>
A dropdown presents a list of options the user can select from and can be used to submit data,
filter, in a menu and so on.
A popover is a non-modal dialog that appears above the content on the screen without losing the context of their original view.
It can contain rich data such as text, selection controls, and buttons. A popover is used with a clickable trigger element and
should position itself relative to where there is free space on the screen.
</Subtitle>

## How to get started
Start by importing the component. If you import the web component version, both the `<cx-dropdown>` component and `<cx-option>` is available for use
from the point of the import and further down the application-tree.
<Source
code={`
Start by importing the component.
``` ts
// Web component
import '@computas/designsystem/dropdown';
import '@computas/designsystem/popover';

// React
import { CxDropdown, CxOption } from '@computas/designsystem/dropdown/react';
`}
language="typescript"
dark
/>
import { CxPopover } from '@computas/designsystem/popover/react';
```

## Browser support
The popover component uses the new [Anchor Position API](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning/Using)
to position the popover relative to the trigger. This API is not supported in Safari and Firefox as writing these docs. If you want to support
all major browsers, please include the official anchor position API polyfill in the `<head>` of your application:
to position the popover relative to the trigger. This API is not supported in Safari and Firefox as of 20th January 2025. If you want to support
all major browsers, please include the official anchor position API polyfill in the `<head>` of your document:
``` html
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
Expand All @@ -39,14 +35,19 @@ all major browsers, please include the official anchor position API polyfill in
Read more about this polyfill [here](https://github.com/oddbird/css-anchor-positioning).

## Default
The default dropdown is an `cx-dropdown` component with a set of `cx-option` child components. Each `cx-option` needs a `value` that contains the
value of the option. If the user selects an option, a `change` event is emitted, containing the selected value (if you use React, the event name is `onChange`).
A popover requires a clickable element to act as the trigger for opening/closing the popover. This trigger is registered through the "trigger"
slot by adding the attribute `slot="trigger"`. The remaining content inside the `cx-popover` element is placed into the popover as its content.
<Canvas of={stories.Default} />

## With header
Add a `header` attribute to the `cx-popover` to display a header to the popover.
Add a `header` attribute to the `cx-popover` to display a header.
<Canvas of={stories.WithHeader} />

## With close button
Add the `withCloseBtn` attribute to display a close button in the popover.
<Canvas of={stories.WithCloseBtn} />

## Autofocus
By default, the popover does not move focus into the popover when it opens. To automatically move the focus into the popover when it opens,
simply add the `autofocus` attribute.
<Canvas of={stories.Autofocus} />
53 changes: 36 additions & 17 deletions packages/lib/components/popover/popover.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@ export const Default: StoryObj = {
<cx-icon name="edit"></cx-icon>
</button>
<p>Edit the settings below:</p>
<label class="cx-form-field">
<div class="cx-form-field__label">E-mail</div>
<div class="cx-form-field__input-container">
<input autofocus type="email">
</div>
</label>
<p>Here comes the popover content</p>
</cx-popover>
`,
};
Expand All @@ -40,13 +34,20 @@ export const WithHeader: StoryObj = {
<cx-icon name="edit"></cx-icon>
</button>
<p>Edit the settings below:</p>
<label class="cx-form-field">
<label class="cx-form-field cx-mb-4">
<div class="cx-form-field__label">E-mail</div>
<div class="cx-form-field__input-container">
<input autofocus type="email">
<input type="email">
</div>
</label>
<div>
<button class="cx-btn__secondary cx-btn__sm cx-mr-2">
Cancel
</button>
<button class="cx-btn__primary cx-btn__sm">
Save
</button>
</div>
</cx-popover>
`,
};
Expand All @@ -58,13 +59,31 @@ export const WithCloseBtn: StoryObj = {
<cx-icon name="edit"></cx-icon>
</button>
<p>Edit the settings below:</p>
<label class="cx-form-field">
<div class="cx-form-field__label">E-mail</div>
<div class="cx-form-field__input-container">
<input autofocus type="email">
</div>
</label>
<p class="cx-mb-4">Would you like to save the changes?</p>
<button class="cx-btn__secondary cx-btn__sm cx-mr-2">
Cancel
</button>
<button class="cx-btn__primary cx-btn__sm">
Save
</button>
</cx-popover>
`,
};

export const Autofocus: StoryObj = {
render: () => html`
<cx-popover autofocus>
<button slot="trigger" class="cx-btn__secondary cx-btn__icon">
<cx-icon name="edit"></cx-icon>
</button>
<p class="cx-mb-4">Would you like to save the changes?</p>
<button class="cx-btn__secondary cx-btn__sm cx-mr-2">
Cancel
</button>
<button class="cx-btn__primary cx-btn__sm">
Save
</button>
</cx-popover>
`,
};
56 changes: 47 additions & 9 deletions packages/lib/components/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ addIcons(close);

import a11yStyles from '../../global-css/a11y.css?inline';
import buttonStyles from '../button/button.css?inline';
import { getFocusableElements } from '../../shared/getFocusableElement';

@customElement('cx-popover')
export class Popover extends LitElement {
Expand Down Expand Up @@ -55,10 +56,9 @@ export class Popover extends LitElement {
opacity: 0;
translate: 0px 6px;
inset: unset;
left: anchor(left);
top: anchor(bottom);
margin: var(--cx-spacing-2) 0 0 0;
position-try-fallbacks: --top;
position-area: bottom span-right;
position-try-fallbacks: --bottom-left, --top-right, --top-left, --center-right, --center-left;
transition:
display 200ms allow-discrete,
overlay 200ms allow-discrete,
Expand All @@ -83,12 +83,30 @@ export class Popover extends LitElement {
}
}
@position-try --top {
inset: unset;
left: anchor(left);
bottom: anchor(top);
@position-try --bottom-left {
position-area: bottom span-left;
margin: var(--cx-spacing-2) 0 0 0;
}
@position-try --top-right {
position-area: top span-right;
margin: 0 0 var(--cx-spacing-2) 0;
}
@position-try --top-left {
position-area: top span-left;
margin: 0 0 var(--cx-spacing-2) 0;
}
@position-try --center-right {
position-area: span-bottom right;
margin: 0 0 0 var(--cx-spacing-2);
}
@position-try --center-left {
position-area: span-bottom left;
margin: 0 var(--cx-spacing-2) 0 0;
}
`,
];

Expand All @@ -101,6 +119,12 @@ export class Popover extends LitElement {
@query('[popover]')
private popoverElement!: HTMLDivElement;

@query('button')
private closeButton!: HTMLButtonElement;

@query('#dialog-content')
private dialogContent!: HTMLSlotElement;

@query('slot[name="trigger"]')
private triggerWrapper!: HTMLSlotElement;

Expand All @@ -118,13 +142,28 @@ export class Popover extends LitElement {
if (event.newState === 'open') {
this.isOpen = true;
this.dispatchEvent(new CustomEvent('open', { bubbles: true, composed: true }));

if (this.autofocus) {
this.focusFirstElement();
}
} else {
(this.triggerWrapper.assignedElements()[0] as HTMLElement).focus();
this.isOpen = false;
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
}
}

private focusFirstElement() {
if (this.withCloseBtn) {
this.closeButton.focus();
} else {
const focusableElements = getFocusableElements(this.dialogContent);
if (focusableElements.length) {
(focusableElements.at(0) as HTMLElement).focus();
}
}
}

render() {
const closeBtn = html`
<button ?hidden=${!this.withCloseBtn} class="cx-btn__tertiary cx-btn__icon" @click=${() => this.popoverElement.hidePopover()}>
Expand All @@ -144,8 +183,7 @@ export class Popover extends LitElement {
<div popover @toggle=${this.popoverToggle}>
${header}
<slot></slot>
<slot id="dialog-content"></slot>
</div>
`;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/lib/shared/getFocusableElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const focusableItemMatcher =
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])';

export const getFocusableElements = (slot: HTMLSlotElement) => {
const focusableElements: Element[] = [
...slot.assignedElements().filter((slotChild) => slotChild.matches(focusableItemMatcher)),
...slot.assignedElements().flatMap((slotChild) => [...slotChild.querySelectorAll(focusableItemMatcher)]),
];

return focusableElements.filter(
(element) => !element.hasAttribute('disabled') && !element.hasAttribute('aria-hidden'),
);
};

0 comments on commit fe8ab44

Please sign in to comment.