@kenlo/sds-design-system 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/components/Accordion/Accordion.css +186 -0
- package/components/Accordion/accordion-variants.js +17 -0
- package/components/Accordion/accordion.marko +264 -0
- package/components/AccordionGroup/AccordionGroup.css +35 -0
- package/components/AccordionGroup/accordion-group.marko +17 -0
- package/components/Alert/Alert.css +104 -0
- package/components/Alert/alert-variants.js +35 -0
- package/components/Alert/alert.marko +94 -0
- package/components/AttachmentList/AttachmentList.css +92 -0
- package/components/AttachmentList/attachment-list.marko +51 -0
- package/components/AttachmentList/attachment-row.marko +167 -0
- package/components/Avatar/Avatar.css +110 -0
- package/components/Avatar/avatar-variants.js +12 -0
- package/components/Avatar/avatar.marko +71 -0
- package/components/Badge/Badge.css +381 -0
- package/components/Badge/badge-dismissible.marko +143 -0
- package/components/Badge/badge-variants.js +35 -0
- package/components/Badge/badge.marko +134 -0
- package/components/BottomSheet/BottomSheet.css +300 -0
- package/components/BottomSheet/bottom-sheet.marko +340 -0
- package/components/BulkActionBar/BulkActionBar.css +88 -0
- package/components/BulkActionBar/bulk-action-bar.marko +186 -0
- package/components/Button/Button.css +340 -0
- package/components/Button/button-variants.js +25 -0
- package/components/Button/button.marko +139 -0
- package/components/Card/Card.css +391 -0
- package/components/Card/card-body.marko +5 -0
- package/components/Card/card-collapsible.marko +94 -0
- package/components/Card/card-footer.marko +12 -0
- package/components/Card/card-header.marko +30 -0
- package/components/Card/card-info-pair.marko +17 -0
- package/components/Card/card-info-row.marko +12 -0
- package/components/Card/card-section.marko +14 -0
- package/components/Card/card-variants.js +24 -0
- package/components/Card/card.marko +124 -0
- package/components/Checkbox/Checkbox.css +249 -0
- package/components/Checkbox/checkbox-variants.js +21 -0
- package/components/Checkbox/checkbox.marko +128 -0
- package/components/ColorPicker/ColorPicker.css +253 -0
- package/components/ColorPicker/color-maths.js +104 -0
- package/components/ColorPicker/color-picker.marko +474 -0
- package/components/DatePicker/DatePicker.css +429 -0
- package/components/DatePicker/date-logic.js +225 -0
- package/components/DatePicker/date-picker.marko +492 -0
- package/components/Drawer/Drawer.css +174 -0
- package/components/Drawer/drawer.marko +262 -0
- package/components/Floating/Floating.css +28 -0
- package/components/Floating/floating-position.js +251 -0
- package/components/Floating/floating.marko +291 -0
- package/components/Form/Form.css +15 -0
- package/components/Form/form-context.js +69 -0
- package/components/Form/form.marko +73 -0
- package/components/GradientPicker/GradientPicker.css +179 -0
- package/components/GradientPicker/gradient-picker.marko +398 -0
- package/components/Grid/Grid.css +161 -0
- package/components/Grid/col.marko +36 -0
- package/components/Grid/grid-variants.js +43 -0
- package/components/Grid/grid.marko +25 -0
- package/components/Header/Header.css +457 -0
- package/components/Header/header.marko +523 -0
- package/components/Icon/Icon.css +44 -0
- package/components/Icon/icon-colors.js +31 -0
- package/components/Icon/icon.marko +56 -0
- package/components/IconButton/IconButton.css +85 -0
- package/components/IconButton/icon-button-variants.js +18 -0
- package/components/IconButton/icon-button.marko +103 -0
- package/components/Incrementer/Incrementer.css +94 -0
- package/components/Incrementer/incrementer.marko +70 -0
- package/components/Input/Input.css +547 -0
- package/components/Input/input-countries.js +109 -0
- package/components/Input/input-masks.js +218 -0
- package/components/Input/input-types.js +194 -0
- package/components/Input/input.marko +530 -0
- package/components/Menu/Menu.css +651 -0
- package/components/Menu/menu-variants.js +89 -0
- package/components/Menu/menu.marko +560 -0
- package/components/Modal/Modal.css +160 -0
- package/components/Modal/modal.marko +296 -0
- package/components/Nav/Nav.css +298 -0
- package/components/Nav/nav-variants.js +23 -0
- package/components/Nav/nav.marko +164 -0
- package/components/Notification/Notification.css +78 -0
- package/components/Notification/notification-variants.js +30 -0
- package/components/Notification/notification.marko +55 -0
- package/components/Pagination/Pagination.css +78 -0
- package/components/Pagination/pagination-pages.js +66 -0
- package/components/Pagination/pagination.marko +141 -0
- package/components/Pill/Pill.css +193 -0
- package/components/Pill/pill-variants.js +16 -0
- package/components/Pill/pill.marko +165 -0
- package/components/Popover/Popover.css +121 -0
- package/components/Popover/popover.marko +96 -0
- package/components/Radio/Radio.css +230 -0
- package/components/Radio/radio-variants.js +16 -0
- package/components/Radio/radio.marko +93 -0
- package/components/RadioGroup/RadioGroup.css +32 -0
- package/components/RadioGroup/radio-group.marko +93 -0
- package/components/SearchBar/SearchBar.css +181 -0
- package/components/SearchBar/search-bar-variants.js +24 -0
- package/components/SearchBar/search-bar.marko +157 -0
- package/components/Select/Select.css +427 -0
- package/components/Select/select-options.js +163 -0
- package/components/Select/select.marko +449 -0
- package/components/Separator/Separator.css +22 -0
- package/components/Separator/separator.marko +14 -0
- package/components/Skeleton/Skeleton.css +100 -0
- package/components/Skeleton/skeleton-avatar.marko +28 -0
- package/components/Skeleton/skeleton-button.marko +22 -0
- package/components/Skeleton/skeleton-image.marko +20 -0
- package/components/Skeleton/skeleton-input.marko +22 -0
- package/components/Skeleton/skeleton-variants.js +16 -0
- package/components/Skeleton/skeleton.marko +53 -0
- package/components/Spinner/Spinner.css +173 -0
- package/components/Spinner/spinner-geometry.js +39 -0
- package/components/Spinner/spinner-overlay.marko +97 -0
- package/components/Spinner/spinner.marko +88 -0
- package/components/StatCard/StatCard.css +90 -0
- package/components/StatCard/stat-card-group.marko +5 -0
- package/components/StatCard/stat-card.marko +57 -0
- package/components/Switch/Switch.css +209 -0
- package/components/Switch/switch.marko +99 -0
- package/components/Table/Table.css +270 -0
- package/components/Table/table-cell.marko +28 -0
- package/components/Table/table-header.marko +27 -0
- package/components/Table/table-logic.js +129 -0
- package/components/Table/table.marko +372 -0
- package/components/Text/Text.css +79 -0
- package/components/Text/text-variants.js +25 -0
- package/components/Text/text.marko +48 -0
- package/components/TextEditor/TextEditor.css +78 -0
- package/components/TextEditor/safe-url.js +50 -0
- package/components/TextEditor/text-editor-commands.js +22 -0
- package/components/TextEditor/text-editor.marko +253 -0
- package/components/Toast/Toast.css +88 -0
- package/components/Toast/toast-api.js +19 -0
- package/components/Toast/toast-store.js +118 -0
- package/components/Toast/toast-variants.js +16 -0
- package/components/Toast/toast.marko +93 -0
- package/components/Toast/toaster.marko +70 -0
- package/components/Toggle/Toggle.css +169 -0
- package/components/Toggle/toggle.marko +93 -0
- package/components/Tooltip/Tooltip.css +115 -0
- package/components/Tooltip/tooltip.marko +77 -0
- package/components/Uploader/Uploader.css +180 -0
- package/components/Uploader/crop-geometry.js +161 -0
- package/components/Uploader/uploader.marko +269 -0
- package/foundations/Scrollbar/Scrollbar.css +26 -0
- package/foundations/attrs.js +27 -0
- package/foundations/breakpoints.js +27 -0
- package/foundations/contrast.js +74 -0
- package/foundations/custom-scheme.js +222 -0
- package/foundations/overlay.js +108 -0
- package/foundations/styles.css +284 -0
- package/foundations/text.js +22 -0
- package/foundations/theme-server.js +81 -0
- package/foundations/theme.js +42 -0
- package/foundations/tokens.css +1656 -0
- package/icons/custom/custom-video-horizontal-filled.marko +6 -0
- package/icons/custom/custom-video-horizontal-regular.marko +6 -0
- package/icons/custom/custom-video-tour-3d-filled.marko +7 -0
- package/icons/custom/custom-video-tour-3d-regular.marko +9 -0
- package/icons/custom/custom-video-vertical-filled.marko +6 -0
- package/icons/custom/custom-video-vertical-regular.marko +6 -0
- package/icons/generated/fluent-add-regular.marko +3 -0
- package/icons/generated/fluent-alert-regular.marko +3 -0
- package/icons/generated/fluent-apps-regular.marko +3 -0
- package/icons/generated/fluent-arrow-download-regular.marko +3 -0
- package/icons/generated/fluent-arrow-left-regular.marko +3 -0
- package/icons/generated/fluent-arrow-right-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-clockwise-regular.marko +3 -0
- package/icons/generated/fluent-arrow-rotate-counterclockwise-regular.marko +3 -0
- package/icons/generated/fluent-border-all-regular.marko +3 -0
- package/icons/generated/fluent-calendar-regular.marko +3 -0
- package/icons/generated/fluent-checkmark-circle-filled.marko +3 -0
- package/icons/generated/fluent-checkmark-regular.marko +3 -0
- package/icons/generated/fluent-chevron-down-regular.marko +3 -0
- package/icons/generated/fluent-chevron-left-regular.marko +3 -0
- package/icons/generated/fluent-chevron-right-regular.marko +3 -0
- package/icons/generated/fluent-chevron-up-regular.marko +3 -0
- package/icons/generated/fluent-circle-half-fill-regular.marko +3 -0
- package/icons/generated/fluent-clock-regular.marko +3 -0
- package/icons/generated/fluent-color-regular.marko +3 -0
- package/icons/generated/fluent-crop-regular.marko +3 -0
- package/icons/generated/fluent-dark-theme-regular.marko +3 -0
- package/icons/generated/fluent-delete-regular.marko +3 -0
- package/icons/generated/fluent-desktop-regular.marko +3 -0
- package/icons/generated/fluent-dismiss-circle-filled.marko +3 -0
- package/icons/generated/fluent-dismiss-regular.marko +3 -0
- package/icons/generated/fluent-document-pdf-regular.marko +3 -0
- package/icons/generated/fluent-document-regular.marko +3 -0
- package/icons/generated/fluent-eye-off-regular.marko +3 -0
- package/icons/generated/fluent-eye-regular.marko +3 -0
- package/icons/generated/fluent-eyedropper-regular.marko +3 -0
- package/icons/generated/fluent-grid-regular.marko +3 -0
- package/icons/generated/fluent-history-regular.marko +3 -0
- package/icons/generated/fluent-home-regular.marko +3 -0
- package/icons/generated/fluent-image-regular.marko +3 -0
- package/icons/generated/fluent-info-regular.marko +3 -0
- package/icons/generated/fluent-link-regular.marko +3 -0
- package/icons/generated/fluent-more-horizontal-regular.marko +3 -0
- package/icons/generated/fluent-navigation-regular.marko +3 -0
- package/icons/generated/fluent-paint-brush-regular.marko +3 -0
- package/icons/generated/fluent-question-circle-regular.marko +3 -0
- package/icons/generated/fluent-search-regular.marko +3 -0
- package/icons/generated/fluent-settings-regular.marko +3 -0
- package/icons/generated/fluent-square-hint-regular.marko +3 -0
- package/icons/generated/fluent-subtract-regular.marko +3 -0
- package/icons/generated/fluent-text-align-center-regular.marko +3 -0
- package/icons/generated/fluent-text-align-left-regular.marko +3 -0
- package/icons/generated/fluent-text-align-right-regular.marko +3 -0
- package/icons/generated/fluent-text-bold-regular.marko +3 -0
- package/icons/generated/fluent-text-bullet-list-regular.marko +3 -0
- package/icons/generated/fluent-text-font-regular.marko +3 -0
- package/icons/generated/fluent-text-font-size-regular.marko +3 -0
- package/icons/generated/fluent-text-italic-regular.marko +3 -0
- package/icons/generated/fluent-text-number-list-ltr-regular.marko +3 -0
- package/icons/generated/fluent-video-regular.marko +3 -0
- package/icons/generated/fluent-warning-filled.marko +3 -0
- package/icons/generated/fluent-weather-moon-regular.marko +3 -0
- package/icons/generated/fluent-weather-sunny-regular.marko +3 -0
- package/icons/generated/fluent-zoom-in-regular.marko +3 -0
- package/icons/generated/fluent-zoom-out-regular.marko +3 -0
- package/icons/manifest.json +61 -0
- package/icons/social/social-facebook-color.marko +7 -0
- package/icons/social/social-facebook.marko +6 -0
- package/icons/social/social-google-color.marko +20 -0
- package/icons/social/social-google.marko +6 -0
- package/icons/social/social-instagram-color.marko +16 -0
- package/icons/social/social-instagram.marko +6 -0
- package/icons/social/social-linkedin-color.marko +8 -0
- package/icons/social/social-linkedin.marko +8 -0
- package/icons/social/social-messenger-color.marko +15 -0
- package/icons/social/social-messenger.marko +6 -0
- package/icons/social/social-pinterest-color.marko +6 -0
- package/icons/social/social-pinterest.marko +6 -0
- package/icons/social/social-telegram-color.marko +7 -0
- package/icons/social/social-telegram.marko +6 -0
- package/icons/social/social-whatsapp-color.marko +8 -0
- package/icons/social/social-whatsapp.marko +7 -0
- package/icons/social/social-x-color.marko +7 -0
- package/icons/social/social-x.marko +6 -0
- package/icons/social/social-youtube-color.marko +7 -0
- package/icons/social/social-youtube.marko +6 -0
- package/marko.json +203 -0
- package/package.json +55 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Menu vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Ported from kdls-design-system v2.5.1 (0f3edf8) components/Menu/Menu.tsx.
|
|
5
|
+
*/
|
|
6
|
+
export const MENU_PLACEMENTS = ['bottom', 'bottomLeft', 'bottomRight'];
|
|
7
|
+
export const MENU_INTENTS = ['primary', 'neutral', 'error', 'success', 'alert'];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Trigger shapes.
|
|
11
|
+
*
|
|
12
|
+
* KDLS spells these as `variant` and offers navButton | pill | avatar | iconButton. All
|
|
13
|
+
* four are here. `custom` is the fifth and is SDS's own: a caller that needs a shape this
|
|
14
|
+
* list does not have supplies the trigger itself, rather than waiting for the list to
|
|
15
|
+
* grow.
|
|
16
|
+
*/
|
|
17
|
+
export const MENU_TRIGGERS = ['navButton', 'pill', 'avatar', 'iconButton', 'custom'];
|
|
18
|
+
|
|
19
|
+
function assertOneOf(label, value, allowed) {
|
|
20
|
+
if (!allowed.includes(value)) {
|
|
21
|
+
throw new Error(`Unknown menu ${label}: "${value}". Allowed: ${allowed.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const assertPlacement = (v) => assertOneOf('placement', v, MENU_PLACEMENTS);
|
|
27
|
+
export const assertTrigger = (v) => assertOneOf('trigger', v, MENU_TRIGGERS);
|
|
28
|
+
export const assertIntent = (v) => (v === undefined ? v : assertOneOf('intent', v, MENU_INTENTS));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalises whatever the caller passed into one list of rows.
|
|
32
|
+
*
|
|
33
|
+
* Two APIs, both of which KDLS had: an `items` array for a menu built from data, and
|
|
34
|
+
* attribute tags for one written out. They produce the same shape so the template only
|
|
35
|
+
* knows about one.
|
|
36
|
+
*/
|
|
37
|
+
export function normaliseItems({ items, item }) {
|
|
38
|
+
const tags = item === undefined ? [] : [].concat(item);
|
|
39
|
+
const defs = items === undefined ? [] : items;
|
|
40
|
+
|
|
41
|
+
const fromTags = tags.map((tag, index) => ({
|
|
42
|
+
key: tag.key ?? `tag-${index}`,
|
|
43
|
+
// `custom` is a row that is not a choice: markup living in the panel, the way KDLS
|
|
44
|
+
// drops a ColorPicker into a Menu.Group. It is not focusable, does not close the
|
|
45
|
+
// menu, and the keyboard steps over it.
|
|
46
|
+
type: tag.separator ? 'divider' : (tag.section ? 'section' : (tag.custom ? 'custom' : undefined)),
|
|
47
|
+
label: tag.label,
|
|
48
|
+
renderBody: tag.renderBody,
|
|
49
|
+
icon: tag.icon,
|
|
50
|
+
intent: tag.intent,
|
|
51
|
+
badge: tag.badge,
|
|
52
|
+
href: tag.href,
|
|
53
|
+
targetBlank: tag.targetBlank,
|
|
54
|
+
active: tag.active,
|
|
55
|
+
disabled: tag.disabled,
|
|
56
|
+
onClick: tag.onClick,
|
|
57
|
+
children: tag.children,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return [...defs, ...fromTags];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A row and, when it is expanded, the rows inside it — as one flat list.
|
|
65
|
+
*
|
|
66
|
+
* The keyboard walks one list. An expandable row that holds three children is four
|
|
67
|
+
* landing places when it is open and one when it is shut, and the arrow keys have to
|
|
68
|
+
* agree with what is on screen: stepping into a collapsed submenu moves the highlight to
|
|
69
|
+
* something nobody can see.
|
|
70
|
+
*
|
|
71
|
+
* `depth` is what the renderer indents by; `parent` is the index of the row that owns it.
|
|
72
|
+
*/
|
|
73
|
+
export function flattenRows(rows, expanded = {}) {
|
|
74
|
+
const out = [];
|
|
75
|
+
rows.forEach((row, index) => {
|
|
76
|
+
const key = row.key ?? index;
|
|
77
|
+
const hasChildren = Array.isArray(row.children) && row.children.length > 0;
|
|
78
|
+
out.push({ ...row, key, depth: 0, expandable: hasChildren, expanded: Boolean(expanded[key]) });
|
|
79
|
+
if (!hasChildren || !expanded[key]) return;
|
|
80
|
+
row.children.forEach((child, childIndex) => {
|
|
81
|
+
out.push({ ...child, key: `${key}-${childIndex}`, depth: 1, parent: key });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Rows a keyboard can land on — a divider or a section header is not one of them. */
|
|
88
|
+
export const isSelectable = (row) =>
|
|
89
|
+
row.type !== 'divider' && row.type !== 'section' && row.type !== 'custom' && !row.disabled;
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
<!-- sds-menu — ported from kdls-design-system v2.5.1 (0f3edf8) components/Menu/Menu.tsx.
|
|
2
|
+
|
|
3
|
+
A trigger and a floating list. Two ways to fill it, both of which KDLS had:
|
|
4
|
+
|
|
5
|
+
<sds-menu label="Ações">
|
|
6
|
+
<@item onClick=onEdit>Editar</@item>
|
|
7
|
+
<@item separator/>
|
|
8
|
+
<@item intent="error" onClick=onDelete>Excluir</@item>
|
|
9
|
+
</sds-menu>
|
|
10
|
+
|
|
11
|
+
<sds-menu label="Ações" items=rows/>
|
|
12
|
+
|
|
13
|
+
KDLS gave its items a React context carrying `close` and `listboxId`, and had them read
|
|
14
|
+
Nav's context as well. Marko 5 has no context, so the menu owns the whole list and renders
|
|
15
|
+
each row itself — see `.claude/component-rules.md` §2c.
|
|
16
|
+
|
|
17
|
+
Not ported in this pass, and each for a stated reason:
|
|
18
|
+
|
|
19
|
+
name, code, agency the profile header, which belongs to the avatar trigger below
|
|
20
|
+
accordion a row that expands inside the panel instead of flying out; it is
|
|
21
|
+
the submenu case in another shape and waits on the same decision
|
|
22
|
+
onChange belongs to the searchable variant, which is not here either
|
|
23
|
+
|
|
24
|
+
submenus a nested floating list with its own focus management; it belongs with
|
|
25
|
+
Popover, which is not ported either
|
|
26
|
+
search needs Input inside the panel, and a filtering decision that is not KDLS's
|
|
27
|
+
to make for us
|
|
28
|
+
pill, avatar trigger shapes that render an sds-pill and an sds-avatar
|
|
29
|
+
tooltip still open on touch behaviour
|
|
30
|
+
|
|
31
|
+
Placement is CSS rather than measured positioning. KDLS computes the panel's position in
|
|
32
|
+
JavaScript; on a page that renders on the server, a menu that only knows where to sit
|
|
33
|
+
after hydration is a menu that jumps. The three placements KDLS offers are all below the
|
|
34
|
+
trigger, so CSS does them — and when Popover lands with real collision detection, this
|
|
35
|
+
grows into it. -->
|
|
36
|
+
|
|
37
|
+
import { assertPlacement, assertTrigger, assertIntent, normaliseItems, flattenRows, isSelectable }
|
|
38
|
+
from "./menu-variants.js";
|
|
39
|
+
|
|
40
|
+
import chevronIcon from "../../icons/generated/fluent-chevron-down-regular.marko";
|
|
41
|
+
// The fallback glyph for an icon trigger with no icon of its own: "more", which is what an
|
|
42
|
+
// unlabelled menu on a row means everywhere it appears.
|
|
43
|
+
import moreIcon from "../../icons/generated/fluent-more-horizontal-regular.marko";
|
|
44
|
+
|
|
45
|
+
class {
|
|
46
|
+
onCreate() {
|
|
47
|
+
this.state = { open: false, activeIndex: -1, expanded: {} };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onMount() {
|
|
51
|
+
// Closing on an outside click is what makes a menu feel like a menu. The listener is
|
|
52
|
+
// on the document because the click that should close it happens outside this tree.
|
|
53
|
+
this.onDocumentClick = (event) => {
|
|
54
|
+
if (!this.isOpen) return;
|
|
55
|
+
|
|
56
|
+
// `this.el` rather than `getEl("root")`, and no optional chaining.
|
|
57
|
+
//
|
|
58
|
+
// `getEl("root")?.contains(…)` reads as safe and is the opposite: when the lookup
|
|
59
|
+
// returns nothing the whole expression is undefined, which is falsy, so the guard
|
|
60
|
+
// falls through and the menu closes itself on the very click that opened it. That
|
|
61
|
+
// is exactly what happened with the pill trigger on the portal's own bar — it
|
|
62
|
+
// opened and shut in one click, and a click that did not reach the document opened
|
|
63
|
+
// it fine, which is what pointed here.
|
|
64
|
+
//
|
|
65
|
+
// A menu that cannot find its own element has no business deciding the click was
|
|
66
|
+
// outside it, so an absent root now means "do nothing" rather than "close".
|
|
67
|
+
const root = this.el ?? this.getEl("root");
|
|
68
|
+
if (!root || root.contains(event.target)) return;
|
|
69
|
+
|
|
70
|
+
this.close();
|
|
71
|
+
};
|
|
72
|
+
document.addEventListener("click", this.onDocumentClick);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onDestroy() {
|
|
76
|
+
document.removeEventListener("click", this.onDocumentClick);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The rows the panel shows, with the children of any expanded row folded in.
|
|
81
|
+
*
|
|
82
|
+
* The keyboard walks one list, so an expandable row has to contribute its children to
|
|
83
|
+
* that list exactly when they are on screen — otherwise the arrows step into a submenu
|
|
84
|
+
* nobody has opened.
|
|
85
|
+
*/
|
|
86
|
+
get rows() {
|
|
87
|
+
return flattenRows(normaliseItems(this.input), this.state.expanded);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Open or close a row that holds others.
|
|
92
|
+
*
|
|
93
|
+
* Inside the panel rather than beside it, which is the `accordion` form KDLS offers and
|
|
94
|
+
* the only one ported. Its default is a floating fly-out that opens to the right, and
|
|
95
|
+
* on a phone there is no right to open into — KDLS knows this and forces the accordion
|
|
96
|
+
* inside a vertical Nav. A design system for public products only needs the one that
|
|
97
|
+
* works in both places.
|
|
98
|
+
*/
|
|
99
|
+
toggleRow(key) {
|
|
100
|
+
const expanded = { ...this.state.expanded };
|
|
101
|
+
if (expanded[key]) delete expanded[key];
|
|
102
|
+
else expanded[key] = true;
|
|
103
|
+
this.state.expanded = expanded;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get isOpen() {
|
|
107
|
+
return this.input.open !== undefined ? this.input.open : this.state.open;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Reports first, then moves its own state — a controlled caller may refuse. */
|
|
111
|
+
setOpen(next) {
|
|
112
|
+
this.input.onOpenChange?.(next);
|
|
113
|
+
if (this.input.open === undefined) this.state.open = next;
|
|
114
|
+
if (next) this.wantsFocus = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* An opened menu puts the caret where the next keystroke belongs.
|
|
119
|
+
*
|
|
120
|
+
* With a header holding a search box, that is the box: the reason to open a list of
|
|
121
|
+
* sixty is to narrow it, and making someone click the field first is asking twice. With
|
|
122
|
+
* no search box it is the first row, so the arrow keys work without a click at all.
|
|
123
|
+
*
|
|
124
|
+
* It runs on update rather than on open: the panel does not exist yet at the moment the
|
|
125
|
+
* state changes, so there is nothing to focus.
|
|
126
|
+
*/
|
|
127
|
+
onUpdate() {
|
|
128
|
+
// A filtered list is a different list. The highlight was an index into the old one, so
|
|
129
|
+
// after typing it pointed at whatever happened to be in that position — arrow-down
|
|
130
|
+
// from the search box landed on a row nobody had looked for.
|
|
131
|
+
const size = this.rows.length;
|
|
132
|
+
if (size !== this.lastSize) {
|
|
133
|
+
this.lastSize = size;
|
|
134
|
+
this.state.activeIndex = this.rows.findIndex((row) => isSelectable(row));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!this.wantsFocus || !this.isOpen) return;
|
|
138
|
+
this.wantsFocus = false;
|
|
139
|
+
|
|
140
|
+
const panel = this.getEl("panel");
|
|
141
|
+
if (!panel) return;
|
|
142
|
+
|
|
143
|
+
const field = panel.querySelector("input, textarea, [contenteditable='true']");
|
|
144
|
+
if (field) {
|
|
145
|
+
field.focus();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.state.activeIndex = this.rows.findIndex((row) => isSelectable(row));
|
|
150
|
+
panel.querySelector("[role='listbox']")?.focus();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
toggle() {
|
|
154
|
+
this.setOpen(!this.isOpen);
|
|
155
|
+
this.state.activeIndex = -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close() {
|
|
159
|
+
this.setOpen(false);
|
|
160
|
+
this.state.activeIndex = -1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Moves the highlight, skipping dividers, section headers and disabled rows. */
|
|
164
|
+
move(step) {
|
|
165
|
+
const rows = this.rows;
|
|
166
|
+
const selectable = rows
|
|
167
|
+
.map((row, index) => (isSelectable(row) ? index : -1))
|
|
168
|
+
.filter((index) => index !== -1);
|
|
169
|
+
if (!selectable.length) return;
|
|
170
|
+
|
|
171
|
+
const current = selectable.indexOf(this.state.activeIndex);
|
|
172
|
+
const next = current === -1
|
|
173
|
+
? (step > 0 ? 0 : selectable.length - 1)
|
|
174
|
+
: (current + step + selectable.length) % selectable.length;
|
|
175
|
+
|
|
176
|
+
this.state.activeIndex = selectable[next];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
handleTriggerKey(event) {
|
|
180
|
+
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
if (!this.isOpen) this.setOpen(true);
|
|
183
|
+
this.move(1);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (event.key === "Escape") this.close();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
handleListKey(event) {
|
|
190
|
+
// The header can hold a text field, and the keys mean different things inside it:
|
|
191
|
+
// Space is a space, not a choice, and an arrow should hand the caret to the list
|
|
192
|
+
// rather than move a highlight nobody is looking at.
|
|
193
|
+
const typing = event.target.matches?.("input, textarea, [contenteditable='true']");
|
|
194
|
+
|
|
195
|
+
if (event.key === "ArrowDown") {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
this.move(1);
|
|
198
|
+
if (typing) this.getEl("panel")?.querySelector("[role='listbox']")?.focus();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (event.key === "ArrowUp") {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
this.move(-1);
|
|
204
|
+
if (typing) this.getEl("panel")?.querySelector("[role='listbox']")?.focus();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (event.key === "Escape") {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
this.close();
|
|
210
|
+
this.getEl("trigger")?.focus();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (event.key === "Enter" || (event.key === " " && !typing)) {
|
|
214
|
+
const row = this.rows[this.state.activeIndex];
|
|
215
|
+
if (row) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
this.pick(row, event);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
pick(row, event) {
|
|
223
|
+
if (row.disabled) {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
row.onClick?.(event);
|
|
228
|
+
// A menu that stays open after a choice is a menu nobody trusts they clicked.
|
|
229
|
+
this.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
$ {
|
|
234
|
+
const {
|
|
235
|
+
label,
|
|
236
|
+
icon,
|
|
237
|
+
trigger = "navButton",
|
|
238
|
+
placement = "bottom",
|
|
239
|
+
triggerActive = false,
|
|
240
|
+
emptyMessage = "Nenhuma opção",
|
|
241
|
+
open: openInput,
|
|
242
|
+
onOpenChange,
|
|
243
|
+
zIndex,
|
|
244
|
+
class: extraClass = "",
|
|
245
|
+
} = input;
|
|
246
|
+
|
|
247
|
+
assertPlacement(placement);
|
|
248
|
+
assertTrigger(input.triggerBody ? "custom" : trigger);
|
|
249
|
+
|
|
250
|
+
const renderable = (value) => (value && value.renderBody) || value;
|
|
251
|
+
|
|
252
|
+
const rows = component.rows;
|
|
253
|
+
for (const row of rows) assertIntent(row.intent);
|
|
254
|
+
|
|
255
|
+
const ariaLabel = input["aria-label"] ?? input.ariaLabel ?? label ?? null;
|
|
256
|
+
if (!ariaLabel) {
|
|
257
|
+
throw new Error("sds-menu needs a `label`, or an `aria-label` when the trigger shows no text.");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// `component.id`, not a random string: Marko's is the same on the server and in the
|
|
261
|
+
// browser, and a random one drawn in the render block is neither.
|
|
262
|
+
const listboxId = `sds-menu-${component.id}`;
|
|
263
|
+
// A visible label and an aria-label together mean the label is a value and the aria-label
|
|
264
|
+
// is its name — the trigger has to say both.
|
|
265
|
+
const namesItself = Boolean(label) && Boolean(input["aria-label"] ?? input.ariaLabel);
|
|
266
|
+
// Controlled when the caller passes `open`, as in Floating and Popover: a menu holding
|
|
267
|
+
// something that opens a fullscreen panel of its own has to be closable by whatever
|
|
268
|
+
// opened that panel.
|
|
269
|
+
const open = openInput !== undefined ? openInput : state.open;
|
|
270
|
+
|
|
271
|
+
const rootClass = [
|
|
272
|
+
"sds-menu",
|
|
273
|
+
`sds-menu--${placement}`,
|
|
274
|
+
open ? "sds-menu--open" : "",
|
|
275
|
+
extraClass,
|
|
276
|
+
].filter(Boolean).join(" ");
|
|
277
|
+
|
|
278
|
+
const triggerClass = [
|
|
279
|
+
"sds-menu__btn",
|
|
280
|
+
`sds-menu__btn--${trigger}`,
|
|
281
|
+
open ? "sds-menu__btn--open" : "",
|
|
282
|
+
triggerActive ? "sds-menu__btn--active" : "",
|
|
283
|
+
].filter(Boolean).join(" ");
|
|
284
|
+
|
|
285
|
+
const rowClass = (row) => [
|
|
286
|
+
"sds-menu__item",
|
|
287
|
+
row.active ? "sds-menu__item--active" : "",
|
|
288
|
+
row.disabled ? "sds-menu__item--disabled" : "",
|
|
289
|
+
row.intent ? `sds-menu__item--${row.intent}` : "",
|
|
290
|
+
].filter(Boolean).join(" ");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
<div key="root" class=rootClass>
|
|
294
|
+
<if(input.triggerBody)>
|
|
295
|
+
<span
|
|
296
|
+
key="trigger"
|
|
297
|
+
class="sds-menu__trigger-custom"
|
|
298
|
+
role="button"
|
|
299
|
+
tabindex="0"
|
|
300
|
+
aria-expanded=(open ? "true" : "false")
|
|
301
|
+
aria-haspopup="listbox"
|
|
302
|
+
aria-controls=(open ? listboxId : null)
|
|
303
|
+
aria-label=ariaLabel
|
|
304
|
+
on-click("toggle")
|
|
305
|
+
on-keydown("handleTriggerKey")
|
|
306
|
+
>
|
|
307
|
+
<${renderable(input.triggerBody)}/>
|
|
308
|
+
</span>
|
|
309
|
+
</if>
|
|
310
|
+
<else-if(trigger === "pill")>
|
|
311
|
+
<!-- A chip that opens a list. KDLS calls this `variant="pill"` and builds its own
|
|
312
|
+
subheader out of it; the pill is a real <button>, and telling it that it opens a
|
|
313
|
+
listbox is what stops it announcing itself as a pressed switch.
|
|
314
|
+
|
|
315
|
+
No `active`: a pill that opens a list is not a filter that is on. See the Pill's
|
|
316
|
+
own header for that rule. -->
|
|
317
|
+
<sds-pill
|
|
318
|
+
key="trigger"
|
|
319
|
+
label=label
|
|
320
|
+
iconLeft=icon
|
|
321
|
+
iconRight=chevronIcon
|
|
322
|
+
onClick=() => component.toggle()
|
|
323
|
+
aria-haspopup="listbox"
|
|
324
|
+
aria-expanded=(open ? "true" : "false")
|
|
325
|
+
aria-controls=(open ? listboxId : null)
|
|
326
|
+
aria-label=(label ? null : ariaLabel)
|
|
327
|
+
/>
|
|
328
|
+
</else-if>
|
|
329
|
+
<else-if(trigger === "iconButton")>
|
|
330
|
+
<!-- A glyph that opens a list — the "…" on a table row, the bell on a header. The
|
|
331
|
+
vocabulary has accepted `iconButton` since the port and there was no branch for
|
|
332
|
+
it: the trigger fell through to the nav button, so a caller who asked for a bare
|
|
333
|
+
icon got a labelled bar with a chevron, and `buttonIcon`, `iconVariant` and
|
|
334
|
+
`iconSize` were read by nothing at all. No error, just a different control.
|
|
335
|
+
|
|
336
|
+
The name is required, as it is on any icon button: a glyph carries none. -->
|
|
337
|
+
<sds-icon-button
|
|
338
|
+
key="trigger"
|
|
339
|
+
variant=(input.iconVariant ?? "neutral")
|
|
340
|
+
size=(input.iconSize ?? "md")
|
|
341
|
+
aria-label=(ariaLabel ?? label ?? "Menu")
|
|
342
|
+
aria-expanded=(open ? "true" : "false")
|
|
343
|
+
aria-haspopup="listbox"
|
|
344
|
+
aria-controls=(open ? listboxId : null)
|
|
345
|
+
onClick=() => component.toggle()
|
|
346
|
+
>
|
|
347
|
+
<@icon><${renderable(input.buttonIcon ?? icon ?? moreIcon)}/></@icon>
|
|
348
|
+
</sds-icon-button>
|
|
349
|
+
</else-if>
|
|
350
|
+
<else-if(trigger === "avatar")>
|
|
351
|
+
<!-- A face that opens a list — the account menu. The avatar is drawn by sds-avatar and
|
|
352
|
+
the button around it is what makes it a control: an <img> with an onclick announces
|
|
353
|
+
itself as an image, and a profile menu nobody can reach by keyboard is a profile
|
|
354
|
+
menu for some people only.
|
|
355
|
+
|
|
356
|
+
The name comes from `ariaLabel`, because the avatar itself is decorative here: its
|
|
357
|
+
initials are already in the panel header below, and hearing them twice says
|
|
358
|
+
nothing the second time. -->
|
|
359
|
+
<button
|
|
360
|
+
key="trigger"
|
|
361
|
+
type="button"
|
|
362
|
+
class="sds-menu__avatar-btn"
|
|
363
|
+
aria-expanded=(open ? "true" : "false")
|
|
364
|
+
aria-haspopup="listbox"
|
|
365
|
+
aria-controls=(open ? listboxId : null)
|
|
366
|
+
aria-label=(ariaLabel ?? name ?? "Conta")
|
|
367
|
+
on-click("toggle")
|
|
368
|
+
on-keydown("handleTriggerKey")
|
|
369
|
+
>
|
|
370
|
+
<sds-avatar src=input.src initials=input.initials size=(input.avatarSize ?? "sm") alt=""/>
|
|
371
|
+
</button>
|
|
372
|
+
</else-if>
|
|
373
|
+
<else>
|
|
374
|
+
<button
|
|
375
|
+
key="trigger"
|
|
376
|
+
type="button"
|
|
377
|
+
class=triggerClass
|
|
378
|
+
aria-expanded=(open ? "true" : "false")
|
|
379
|
+
aria-haspopup="listbox"
|
|
380
|
+
aria-controls=(open ? listboxId : null)
|
|
381
|
+
aria-label=(label ? null : ariaLabel)
|
|
382
|
+
aria-labelledby=(namesItself ? `${listboxId}-name ${listboxId}-value` : null)
|
|
383
|
+
on-click("toggle")
|
|
384
|
+
on-keydown("handleTriggerKey")
|
|
385
|
+
>
|
|
386
|
+
<if(icon)>
|
|
387
|
+
<span class="sds-menu__btn-icon" aria-hidden="true">
|
|
388
|
+
<sds-icon><${renderable(icon)}/></sds-icon>
|
|
389
|
+
</span>
|
|
390
|
+
</if>
|
|
391
|
+
<!-- When the visible label is a *value* rather than a name — a font size reading
|
|
392
|
+
"16" — the caller says what it is with aria-label and both are pointed at, so a
|
|
393
|
+
screen reader hears "Tamanho do texto 16" instead of "16, button". An aria-label
|
|
394
|
+
on its own would replace the value; the visible label on its own leaves the
|
|
395
|
+
value with nothing to say what it means. Same as the Select's nav trigger. -->
|
|
396
|
+
<if(namesItself)>
|
|
397
|
+
<span id=`${listboxId}-name` class="sds-visually-hidden">${ariaLabel}</span>
|
|
398
|
+
</if>
|
|
399
|
+
<if(label)>
|
|
400
|
+
<span id=(namesItself ? `${listboxId}-value` : null) class="sds-menu__btn-label">${label}</span>
|
|
401
|
+
</if>
|
|
402
|
+
<span class="sds-menu__btn-chevron" aria-hidden="true">
|
|
403
|
+
<sds-icon size="sm"><fluent-chevron-down-regular/></sds-icon>
|
|
404
|
+
</span>
|
|
405
|
+
</button>
|
|
406
|
+
</else>
|
|
407
|
+
|
|
408
|
+
<if(open)>
|
|
409
|
+
<div
|
|
410
|
+
key="panel"
|
|
411
|
+
class="sds-menu__panel"
|
|
412
|
+
style=(zIndex !== undefined ? `z-index:${zIndex}` : null)
|
|
413
|
+
on-keydown("handleListKey")
|
|
414
|
+
>
|
|
415
|
+
<!-- The list is wrapped, not placed directly in the panel. The wrapper is what
|
|
416
|
+
carries the panel's vertical padding and, with the panel's 480px cap, what
|
|
417
|
+
scrolls: without it a long menu is clipped at the cap with no way to reach the
|
|
418
|
+
rest of it. -->
|
|
419
|
+
<if(input.header)>
|
|
420
|
+
<div class="sds-menu__header">
|
|
421
|
+
<${input.header}/>
|
|
422
|
+
</div>
|
|
423
|
+
</if>
|
|
424
|
+
|
|
425
|
+
<div class="sds-menu__panel-body">
|
|
426
|
+
<!-- The list holds focus and names the row the arrows are on. Without
|
|
427
|
+
aria-activedescendant a screen reader hears the list and never the row. -->
|
|
428
|
+
<ul
|
|
429
|
+
class="sds-menu__list"
|
|
430
|
+
id=listboxId
|
|
431
|
+
role="listbox"
|
|
432
|
+
aria-label=ariaLabel
|
|
433
|
+
aria-activedescendant=(state.activeIndex >= 0 ? `${listboxId}-row-${state.activeIndex}` : null)
|
|
434
|
+
tabindex="-1"
|
|
435
|
+
>
|
|
436
|
+
<if(!rows.length)>
|
|
437
|
+
<li class="sds-menu__empty">${emptyMessage}</li>
|
|
438
|
+
</if>
|
|
439
|
+
|
|
440
|
+
<for|row, index| of=rows>
|
|
441
|
+
<if(row.type === "divider")>
|
|
442
|
+
<li class="sds-menu__separator" role="separator"/>
|
|
443
|
+
</if>
|
|
444
|
+
<else-if(row.type === "section")>
|
|
445
|
+
<li class="sds-menu__group-title" role="presentation">${row.label}</li>
|
|
446
|
+
</else-if>
|
|
447
|
+
<else-if(row.type === "custom")>
|
|
448
|
+
<!-- Markup in the panel rather than a choice in it — KDLS drops a
|
|
449
|
+
ColorPicker into a Menu.Group the same way. mousedown is stopped here
|
|
450
|
+
because the thing inside opens a panel of its own, and a click out
|
|
451
|
+
there would otherwise read as a click outside this one. -->
|
|
452
|
+
<li class="sds-menu__custom" role="presentation"
|
|
453
|
+
on-mousedown((event) => event.stopPropagation())>
|
|
454
|
+
<${row.renderBody}/>
|
|
455
|
+
</li>
|
|
456
|
+
</else-if>
|
|
457
|
+
<else>
|
|
458
|
+
<li role="none" class=(row.depth ? "sds-menu__child" : null)>
|
|
459
|
+
<macro|{ }| name="rowContent">
|
|
460
|
+
<if(row.icon)>
|
|
461
|
+
<span class="sds-menu__item-icon" aria-hidden="true">
|
|
462
|
+
<sds-icon size="sm"><${renderable(row.icon)}/></sds-icon>
|
|
463
|
+
</span>
|
|
464
|
+
</if>
|
|
465
|
+
<!-- An item can explain itself two ways, as in KDLS: the label becomes
|
|
466
|
+
the trigger, or a separate info affordance sits beside it. -->
|
|
467
|
+
<if(row.tooltip && !row.tooltipIcon)>
|
|
468
|
+
<sds-tooltip content=row.tooltip placement=(row.tooltipPosition ?? "top")>
|
|
469
|
+
<span class="sds-menu__item-label">
|
|
470
|
+
<if(row.renderBody)><${row.renderBody}/></if>
|
|
471
|
+
<else>${row.label}</else>
|
|
472
|
+
</span>
|
|
473
|
+
</sds-tooltip>
|
|
474
|
+
</if>
|
|
475
|
+
<else>
|
|
476
|
+
<span class="sds-menu__item-label">
|
|
477
|
+
<if(row.renderBody)><${row.renderBody}/></if>
|
|
478
|
+
<else>${row.label}</else>
|
|
479
|
+
</span>
|
|
480
|
+
</else>
|
|
481
|
+
<if(row.tooltip && row.tooltipIcon)>
|
|
482
|
+
<span class="sds-menu__item-tooltip-icon">
|
|
483
|
+
<sds-tooltip content=row.tooltip placement=(row.tooltipPosition ?? "top")/>
|
|
484
|
+
</span>
|
|
485
|
+
</if>
|
|
486
|
+
<if(row.badge)>
|
|
487
|
+
<sds-badge label=row.badge variant="scheme-color" appearance="tone-50" size="sm"/>
|
|
488
|
+
</if>
|
|
489
|
+
</macro>
|
|
490
|
+
|
|
491
|
+
<if(row.expandable)>
|
|
492
|
+
<!-- A row that opens the rows under it, in the panel rather than beside
|
|
493
|
+
it. A real <button>, so Enter and Space come from the browser rather
|
|
494
|
+
than from a keydown handler; `role="option"` because a listbox may
|
|
495
|
+
only hold options and groups, and ARIA allows `aria-expanded` on an
|
|
496
|
+
option. KDLS uses a <div role="option"> with its own key handling,
|
|
497
|
+
and nests a second listbox inside the first, which is not valid. -->
|
|
498
|
+
<button
|
|
499
|
+
type="button"
|
|
500
|
+
id=`${listboxId}-row-${index}`
|
|
501
|
+
class=`${rowClass(row)} sds-menu__item--expandable`
|
|
502
|
+
role="option"
|
|
503
|
+
aria-selected="false"
|
|
504
|
+
aria-expanded=(row.expanded ? "true" : "false")
|
|
505
|
+
disabled=(row.disabled || null)
|
|
506
|
+
data-active=(index === state.activeIndex ? "true" : null)
|
|
507
|
+
on-click(() => component.toggleRow(row.key))
|
|
508
|
+
>
|
|
509
|
+
<rowContent/>
|
|
510
|
+
<span class="sds-menu__item-chevron" aria-hidden="true">
|
|
511
|
+
<sds-icon size="sm"><fluent-chevron-down-regular/></sds-icon>
|
|
512
|
+
</span>
|
|
513
|
+
</button>
|
|
514
|
+
</if>
|
|
515
|
+
<else-if(row.href)>
|
|
516
|
+
<a
|
|
517
|
+
id=`${listboxId}-row-${index}`
|
|
518
|
+
class=rowClass(row)
|
|
519
|
+
role="option"
|
|
520
|
+
aria-selected=(row.active ? "true" : "false")
|
|
521
|
+
aria-disabled=(row.disabled ? "true" : null)
|
|
522
|
+
data-active=(index === state.activeIndex ? "true" : null)
|
|
523
|
+
href=(row.disabled ? null : row.href)
|
|
524
|
+
target=(row.targetBlank ? "_blank" : null)
|
|
525
|
+
rel=(row.targetBlank ? "noopener noreferrer" : null)
|
|
526
|
+
on-click("pick", row)
|
|
527
|
+
>
|
|
528
|
+
<rowContent/>
|
|
529
|
+
</a>
|
|
530
|
+
</else-if>
|
|
531
|
+
<else>
|
|
532
|
+
<button
|
|
533
|
+
type="button"
|
|
534
|
+
id=`${listboxId}-row-${index}`
|
|
535
|
+
class=rowClass(row)
|
|
536
|
+
role="option"
|
|
537
|
+
aria-selected=(row.active ? "true" : "false")
|
|
538
|
+
disabled=(row.disabled || null)
|
|
539
|
+
data-active=(index === state.activeIndex ? "true" : null)
|
|
540
|
+
on-click("pick", row)
|
|
541
|
+
>
|
|
542
|
+
<rowContent/>
|
|
543
|
+
</button>
|
|
544
|
+
</else>
|
|
545
|
+
</li>
|
|
546
|
+
</else>
|
|
547
|
+
</for>
|
|
548
|
+
</ul>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<!-- Pinned below the list rather than inside it, so it stays put while the rows
|
|
552
|
+
scroll — which is the whole point of a footer on a menu that can be long. -->
|
|
553
|
+
<if(input.footer)>
|
|
554
|
+
<div class="sds-menu__panel-footer">
|
|
555
|
+
<${input.footer}/>
|
|
556
|
+
</div>
|
|
557
|
+
</if>
|
|
558
|
+
</div>
|
|
559
|
+
</if>
|
|
560
|
+
</div>
|