phonograph/svelte/src/basic-dropdown.webc.svelte
2025-12-19 20:19:35 +00:00

124 lines
3.3 KiB
Svelte

<svelte:options
customElement={{
// `shadowRoot` field must remain as the default, else named slots break.
props: {
button_aria_label: { attribute: "button-aria-label" },
alignment: { attribute: "alignment" },
},
tag: "basic-dropdown",
}}
/>
<!--
@component
A button with an associated popover, which can be styled for a variety of
purposes, such as menus, confirmation popups, and so on.
When used as a web component, this component is rendered with a shadow root in
order to correctly support named slots. As a result, it bundles its own
stylesheet, which is merely a copy of the "main" application stylesheet.
## Props
- `alignment`: When set to "right", the popover aligns with the right edge of
the trigger. Otherwise, it aligns with the left.
-->
<script lang="ts">
type Props = {
alignment?: string;
button_aria_label?: string;
on_toggle?(ev: ToggleEvent): unknown;
};
let { alignment, button_aria_label, on_toggle }: Props = $props();
let popover_element: HTMLElement | undefined = $state();
// Hacky workaround because as of September 2025 implicit anchor association
// is still pretty broken, at least in Firefox.
let anchor_name = $state(`--anchor-${Math.floor(Math.random() * 1000000)}`);
let popover_left = $derived(
alignment?.toLocaleLowerCase("en-US") === "right"
? "unset"
: "anchor(left)",
);
let popover_right = $derived(
alignment?.toLocaleLowerCase("en-US") === "right"
? "anchor(right)"
: "unset",
);
$effect(() => {
if (on_toggle) {
popover_element?.addEventListener("toggle", on_toggle);
return () => {
popover_element?.removeEventListener("toggle", on_toggle);
};
}
});
</script>
<button
aria-label={button_aria_label}
class="basic-dropdown__button"
id="dropdown-button"
onclick={() => {
popover_element?.showPopover();
}}
style:anchor-name={anchor_name}
type="button"
>
<slot name="button-contents"></slot>
</button>
<div
aria-labelledby="dropdown-button"
bind:this={popover_element}
class="basic-dropdown__popover"
style:left={popover_left}
style:position-anchor={anchor_name}
style:right={popover_right}
popover="auto"
>
<slot name="popover"></slot>
</div>
<style lang="css">
.basic-dropdown__button {
appearance: none;
box-sizing: border-box;
cursor: pointer;
font-weight: inherit;
background: var(--button-background);
border: solid 1px var(--button-border-color);
border-radius: var(--button-border-radius);
box-shadow: var(--button-shadow);
color: var(--button-color);
font-family: var(--button-font-family);
font-size: var(--button-font-size);
padding: var(--button-padding);
text-decoration: none;
transition: background 0.2s ease;
&:hover {
border-color: oklch(from var(--button-border-color) calc(l * 0.95) c h);
background: oklch(from var(--button-background) calc(l * 0.95) c h);
}
}
.basic-dropdown__popover:popover-open {
background: #fff;
border: solid 1px var(--popover-border-color);
border-radius: var(--default-border-radius--rounded);
box-shadow: var(--popover-shadow);
display: block;
margin: unset;
max-height: 90vh;
overflow: auto;
padding: 0;
position: absolute;
top: anchor(bottom);
}
</style>