ReactionPicker
A row of emoji reactions with counts, one tap to react or take it back, and a menu to add another.
Demo
Tap one to react.
The second is single: picking one moves your reaction.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/reaction_picker.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/reaction_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/reaction_picker.qwe
component ReactionPickerDemoLive() { state last = null <div.demo-stack> <ReactionPicker id="react-demo" name="reaction" reactions={[ { emoji: "👍", label: "Thumbs up", count: 12, mine: true }, { emoji: "❤️", label: "Heart", count: 4 }, { emoji: "🎉", label: "Party", count: 1, mine: true }, ]} choices={[{ emoji: "👍", label: "Thumbs up" }, { emoji: "❤️", label: "Heart" }, { emoji: "🎉", label: "Party" }, { emoji: "😄", label: "Laugh" }, { emoji: "🤔", label: "Thinking" }, { emoji: "🚀", label: "Rocket" }]} on_change={(e, on) => { last = `${on ? "Added" : "Removed"} ${e}` }}/> <p.demo-note>{last ?? "Tap one to react."}</p> </div>}view ReactionPickerDemo() = <div.demo-stack> <ReactionPickerDemoLive/> <ReactionPicker id="react-demo-one" single={true} size="sm" reactions={[ { emoji: "👍", label: "Agree", count: 8, mine: true }, { emoji: "👎", label: "Disagree", count: 2 }, { emoji: "🤷", label: "Not sure", count: 1 }, ]}/> <p.demo-note>The second is single: picking one moves your reaction.</p> </div>Source
ui/reaction_picker.qwe
// ReactionPicker: a row of emoji reactions with how many gave each, one tap to add// yours or take it back; and, with `choices`, a button that offers more emojis to// react with. Each chip is a toggle (aria-pressed), named by the emoji's label.//// <ReactionPicker reactions={[{ emoji: "👍", label: "Thumbs up", count: 12, mine: true }, { emoji: "🎉", label: "Party", count: 3 }]}/>// <ReactionPicker id="r7" reactions={rs} choices={["👍", "❤️", "🎉", "😄"]} on_change={(emoji, on) => { react(post, emoji, on) }}/>//// reactions: a list of { emoji, label?, count, mine?: true }: label is what screen// readers call it (the emoji itself when left out). choices: emojis the add button// offers (strings, or { emoji, label }); those already in the row are left out.// single: only one reaction at a time, so picking another moves yours.// id: the add button's menu id, unique on the page (needed with choices).// name: in a <form>, each emoji of yours is sent as name. size: "sm" or "md".// on_change(emoji, on): inside one of your components, given the emoji and whether// you now react with it.import ui/theme.qwe use ui_clslet reaction_picker_css = css`.ui-reaction-picker { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; vertical-align: middle; font: 500 13px/1 var(--ui-font); }.ui-reaction-picker-chip { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px 0 8px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); color: var(--ui-fg); font: inherit; font-variant-numeric: tabular-nums; cursor: pointer; transition: background .15s, border-color .15s, color .15s, transform .15s;}.ui-reaction-picker-chip:hover { background: var(--ui-subtle); border-color: var(--ui-strong); }.ui-reaction-picker-chip:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reaction-picker-chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)); color: var(--ui-accent); }.ui-reaction-picker-chip:active { transform: scale(.96); }.ui-reaction-picker-emoji { font-size: 16px; line-height: 1; }.ui-reaction-picker-add { padding: 0 9px; color: var(--ui-muted); }.ui-reaction-picker-add svg { width: 16px; height: 16px; }.ui-reaction-picker-menu { display: flex; flex-wrap: wrap; gap: 2px; width: max-content; max-width: min(280px, calc(100vw - 16px)); margin: auto; padding: 6px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow-lg);}.ui-reaction-picker-choice { display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0; border-radius: var(--ui-radius-sm); background: transparent; font-size: 20px; line-height: 1; cursor: pointer;}.ui-reaction-picker-choice:hover { background: var(--ui-subtle); }.ui-reaction-picker-choice:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-reaction-picker-menu:popover-open { animation: ui-reaction-picker-in .12s ease-out; }@keyframes ui-reaction-picker-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) { .ui-reaction-picker-menu { inset: auto; margin: 4px 0; position-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline; }}@supports not (position-area: bottom) { .ui-reaction-picker-menu::backdrop { background: var(--ui-overlay); } }.ui-reaction-picker-sm .ui-reaction-picker-chip { height: 26px; padding: 0 8px 0 7px; font-size: 12px; }.ui-reaction-picker-sm .ui-reaction-picker-emoji { font-size: 14px; }@media (prefers-reduced-motion: reduce) { .ui-reaction-picker-chip { transition: none; } .ui-reaction-picker-menu:popover-open { animation: none; } }`fn reaction_picker_choices(choices: list) -> list = choices | map((c) => type_of(c) == "map" ? { emoji: c.emoji, label: get(c, "label") ?? c.emoji } : { emoji: c, label: c })component ReactionPicker(reactions: list = [], choices: list = [], single: bool = false, id: str = "ui-reaction-picker", name: str? = null, size: str = "md", on_change = null, class: str? = null) { style global reaction_picker_css state rows = reactions | map((r) => { emoji: r.emoji, label: get(r, "label") ?? r.emoji, count: get(r, "count") ?? 0, mine: get(r, "mine") == true }) let toggle = (emoji: str) => { let was = find(rows, (r) => r.emoji == emoji) let on = was == null || !was.mine rows = rows | map((r) => r.emoji == emoji ? { ...r, mine: on, count: r.count + (on ? 1 : -1) } : single && on && r.mine ? { ...r, mine: false, count: r.count - 1 } : r) | filter((r) => r.count > 0) if was == null { let c = find(reaction_picker_choices(choices), (c) => c.emoji == emoji) rows = rows | push({ emoji, label: c != null ? c.label : emoji, count: 1, mine: true }) } on_change && on_change(emoji, on) } let mine = rows | filter((r) => r.mine) | map((r) => r.emoji) let offered = reaction_picker_choices(choices) | filter((c) => !any(rows, (r) => r.emoji == c.emoji)) <div class={ui_cls(["ui-reaction-picker", size == "sm" && "ui-reaction-picker-sm", class])} role="group" aria-label="Reactions"> {for r in rows => <button.ui-reaction-picker-chip key={r.emoji} type="button" aria-pressed={r.mine ? "true" : "false"} aria-label={`${r.label}, ${r.count}`} title={r.label} @click={() => { toggle(r.emoji) }}> <span.ui-reaction-picker-emoji aria-hidden="true">{r.emoji}</span> <span aria-hidden="true">{r.count}</span> </button>} {len(offered) > 0 && <span style={`anchor-name: --${id}`}> <button.ui-reaction-picker-chip.ui-reaction-picker-add type="button" popovertarget={`${id}-menu`} aria-label="Add a reaction" title="Add a reaction"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M8.5 14.5a4.5 4.5 0 0 0 7 0M9 10h.01M15 10h.01"/></svg> <span aria-hidden="true">+</span> </button> <div.ui-reaction-picker-menu id={`${id}-menu`} popover="auto" role="group" aria-label="Reactions to add" style={`position-anchor: --${id}`}> {for c in offered => <button.ui-reaction-picker-choice key={c.emoji} type="button" aria-label={c.label} title={c.label} popovertarget={`${id}-menu`} popovertargetaction="hide" @click={() => { toggle(c.emoji) }}>{c.emoji}</button>} </div> </span>} {for e in (name != null ? mine : []) => <input key={e} type="hidden" name={name} value={e}/>} </div>}Related
BookmarkButtonA save-for-later toggle whose bookmark fills when pressed, with or without its label.8 views · 8 downloadsFollowButtonFollow becomes Following once pressed, and says Unfollow on hover, as the social sites do.5 views · 7 downloadsLikeButtonA heart that toggles with a count that follows it, a real toggle for screen readers and forms.5 views · 4 downloadsShareButtonShares a link through the system's share sheet, or copies it where the browser has none, and says which.6 views · 5 downloadsVoteButtonsUp and down votes around a score that follows them, as on a forum, in a column or a row.5 views · 6 downloadsButtonA button, or a link that looks like one: four variants, three tones, three sizes, loading.7 views · 7 downloads
Thread
Thread 0 posts
On ReactionPicker: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.