lily-design-system-angular-share-picker
v0.2.0
Published
Lily Design System Angular 20 share picker: an icon button opening the native share sheet, or a disclosure of consumer-supplied links plus copy-to-clipboard. Headless, SSR-safe, no CSS.
Maintainers
Readme
SharePicker (Angular helper)
A headless Angular 20 share control: a single-glyph button (➤) that opens the native share sheet where the browser has one, and otherwise shows a list of destinations you supply, plus copy the page URL.
The single source of truth is spec/index.md. This file is the human-readable guide.
Install
import {
SharePicker,
SharePickerIcon,
canShareNatively,
type ShareTarget,
} from "./lily-design-system-angular-share-picker";SharePicker is a standalone component — add it to a component's
imports, not to an NgModule.
Quick start
import { ChangeDetectionStrategy, Component } from "@angular/core";
import {
SharePicker,
type ShareTarget,
} from "../lily-design-system-angular-share-picker";
@Component({
selector: "app-article",
standalone: true,
imports: [SharePicker],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<lily-share-picker
label="Share this page"
title="An article worth reading"
[targets]="targets"
copyLabel="Copy link"
copiedLabel="Link copied"
copyFailedLabel="Could not copy — copy it from the address bar"
(share)="onShare($event)"
/>
`,
})
export class ArticleComponent {
readonly targets: ShareTarget[] = [
{
id: "mastodon",
label: "Mastodon",
href: (url, title) =>
`https://mastodon.social/share?text=${encodeURIComponent(title)}%20${encodeURIComponent(url)}`,
},
{
id: "email",
label: "Email",
href: (url, title) =>
`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`,
newTab: false,
},
];
onShare(event: { targetId: string; url: string }): void {
console.log(event.targetId, event.url);
}
}url defaults to the current page, so the common case needs no wiring.
You supply the destinations
This package ships no social-network URLs. That is deliberate: which
networks belong in your product is an editorial and privacy decision, the
share endpoints change, and networks die. You pass targets, so the
labels localise with the rest of your copy and no third-party endpoint is
baked into a design system.
href is a function, so you own the whole URL and its encoding:
{ id: "linkedin", label: "LinkedIn",
href: (url) => `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}` }Native share sheet
With strategy="auto" (the default), pressing the button on a device
with navigator.share opens the OS sheet — the user gets their real
installed apps, and nothing is disclosed to a third party by the act of
opening it. Where there is no sheet, the list opens instead.
This means behaviour differs by platform, which is worth knowing when
you write help text or test scripts. Force one path with
strategy="list" or strategy="native".
A dismissed sheet ends the interaction — the list does not then pop open, which would resurrect UI the user just dismissed.
Copy to clipboard
Supply copyLabel and a copy item appears. There is no default label,
because a default would be a hardcoded English string. copiedLabel and
copyFailedLabel are announced in a polite live region — copying is
otherwise silent, so without them the user gets no confirmation.
Failure is handled, not assumed away: a denied permission, an insecure
context, or a browser with no async clipboard all announce
copyFailedLabel rather than throwing.
Why links, not a menu
Destinations render as real <a> elements, not role="menuitem". A
menuitem role strips middle-click, open-in-new-tab, and copy-link-address
— affordances users genuinely reach for on a share list. The WAI-ARIA APG
suggests a disclosure when the items are links. Copy is a real action, so
it is a <button>.
Custom glyph
Project an <ng-template> to replace the ➤ glyph inside the trigger. The
optional SharePickerIcon marker directive gives the let- variable a
type:
<lily-share-picker label="Share" [targets]="targets">
<ng-template lilySharePickerIcon let-args>
{{ args.open ? "Close" : "Share" }}
</ng-template>
</lily-share-picker>The context is ChildArgs — { open, url } — available both as
$implicit and as named properties.
Inputs and outputs
Full table in spec/index.md §4.1.
Required: label. Everything else is optional.
Outputs are Angular output()s rather than callback inputs:
(share) emits { targetId, url }, (copy) emits the URL, and
(nativeShare) emits the URL.
Accessibility
- The glyph is
aria-hidden; the name comes fromaria-label, which also names the list, so a screen reader entering it hears what it is for. Escapecloses and returns focus to the trigger; arrows move between items and clamp;Home/Endjump;Tabcloses via the trigger so the default Tab proceeds from the picker's position.- The status region is polite and empty on load.
- Tradeoff: an icon-only control's name rests entirely on
aria-label— there is no visible text fallback. See docs/accessibility.md.
Styling
Class hooks: .share-picker (root), .share-picker-button,
.share-picker-icon, .share-picker-list, .share-picker-list-item,
.share-picker-target, .share-picker-copy, .share-picker-status.
The package ships no CSS. The root themes/ stylesheets style the button
and popup, including the optical glyph sizing that keeps ➤ visually the
same size as the other helpers' glyphs.
Tests
npx vitest run lily-design-system-angular-share-picker from the catalog
root — 49 cases, one or more per §7 clause.
See also
- examples/ — runnable standalone components.
- docs/accessibility.md — what the control does well, and what it costs.
- spec/index.md — the canonical contract.
- CHANGELOG.md — release history.
License
Dual-licensed under MIT or Apache-2.0 or GPL-2.0 or GPL-3.0 or BSD-3-Clause. Contact [email protected] for other terms.
Lily™ and Lily Design System™ are trademarks.
