@slotted/styles 0.0.0-stage → 0.1.0-beta.0
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/LICENSE +21 -0
- package/README.md +10 -2
- package/package.json +276 -3
- package/src/accordion/accordion.css +96 -0
- package/src/accordion/accordion.tokens.json +14 -0
- package/src/action-bar/action-bar.css +455 -0
- package/src/action-bar/action-bar.tokens.json +50 -0
- package/src/action-sheet/action-sheet.css +462 -0
- package/src/action-sheet/action-sheet.tokens.json +59 -0
- package/src/alert/alert.css +764 -0
- package/src/alert/alert.tokens.json +77 -0
- package/src/app-shell/app-shell.css +1899 -0
- package/src/app-shell/app-shell.tokens.json +88 -0
- package/src/aspect-ratio/aspect-ratio.css +131 -0
- package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
- package/src/attachment/attachment.css +906 -0
- package/src/attachment/attachment.tokens.json +85 -0
- package/src/avatar/avatar.css +598 -0
- package/src/avatar/avatar.tokens.json +64 -0
- package/src/avatar-group/avatar-group.css +268 -0
- package/src/avatar-group/avatar-group.tokens.json +16 -0
- package/src/badge/badge.css +462 -0
- package/src/badge/badge.tokens.json +58 -0
- package/src/breadcrumb/breadcrumb.css +441 -0
- package/src/breadcrumb/breadcrumb.tokens.json +50 -0
- package/src/button/button.css +754 -0
- package/src/button/button.tokens.json +72 -0
- package/src/button-group/button-group.css +455 -0
- package/src/button-group/button-group.tokens.json +11 -0
- package/src/calendar/calendar.css +846 -0
- package/src/calendar/calendar.tokens.json +67 -0
- package/src/card/card.css +602 -0
- package/src/card/card.tokens.json +68 -0
- package/src/carousel/carousel.css +860 -0
- package/src/carousel/carousel.tokens.json +62 -0
- package/src/cascader/cascader.css +1320 -0
- package/src/cascader/cascader.tokens.json +103 -0
- package/src/chart/chart.css +1473 -0
- package/src/chart/chart.tokens.json +107 -0
- package/src/checkbox/checkbox.css +1011 -0
- package/src/checkbox/checkbox.tokens.json +102 -0
- package/src/checkbox-group/checkbox-group.css +70 -0
- package/src/checkbox-group/checkbox-group.tokens.json +6 -0
- package/src/code-block/code-block.css +555 -0
- package/src/code-block/code-block.tokens.json +69 -0
- package/src/collapsible/collapsible.css +546 -0
- package/src/collapsible/collapsible.tokens.json +55 -0
- package/src/color-picker/color-picker.css +607 -0
- package/src/color-picker/color-picker.tokens.json +58 -0
- package/src/column-manager/column-manager.css +452 -0
- package/src/column-manager/column-manager.tokens.json +43 -0
- package/src/combobox/combobox.css +1048 -0
- package/src/combobox/combobox.tokens.json +112 -0
- package/src/command-palette/command-palette.css +1000 -0
- package/src/command-palette/command-palette.tokens.json +75 -0
- package/src/container/container.css +198 -0
- package/src/container/container.tokens.json +18 -0
- package/src/context-menu/context-menu.css +104 -0
- package/src/context-menu/context-menu.tokens.json +11 -0
- package/src/currency-field/currency-field.css +787 -0
- package/src/currency-field/currency-field.tokens.json +80 -0
- package/src/dashboard-grid/dashboard-grid.css +748 -0
- package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
- package/src/data-grid/data-grid.css +2484 -0
- package/src/data-grid/data-grid.tokens.json +100 -0
- package/src/data-list/data-list.css +455 -0
- package/src/data-list/data-list.tokens.json +35 -0
- package/src/date-field/date-field.css +560 -0
- package/src/date-field/date-field.tokens.json +80 -0
- package/src/date-picker/date-picker.css +420 -0
- package/src/date-picker/date-picker.tokens.json +49 -0
- package/src/date-range-picker/date-range-picker.css +983 -0
- package/src/date-range-picker/date-range-picker.tokens.json +109 -0
- package/src/datetime-field/datetime-field.css +850 -0
- package/src/datetime-field/datetime-field.tokens.json +91 -0
- package/src/datetime-picker/datetime-picker.css +843 -0
- package/src/datetime-picker/datetime-picker.tokens.json +78 -0
- package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
- package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
- package/src/description-list/description-list.css +675 -0
- package/src/description-list/description-list.tokens.json +66 -0
- package/src/detail-panel/detail-panel.css +329 -0
- package/src/detail-panel/detail-panel.tokens.json +33 -0
- package/src/dialog/dialog.css +968 -0
- package/src/dialog/dialog.tokens.json +66 -0
- package/src/divider/divider.css +131 -0
- package/src/divider/divider.tokens.json +18 -0
- package/src/drawer/drawer.css +1303 -0
- package/src/drawer/drawer.tokens.json +75 -0
- package/src/entity-picker/entity-picker.css +1367 -0
- package/src/entity-picker/entity-picker.tokens.json +118 -0
- package/src/event-calendar/event-calendar.css +2148 -0
- package/src/event-calendar/event-calendar.tokens.json +87 -0
- package/src/field/field.css +707 -0
- package/src/field/field.tokens.json +58 -0
- package/src/field-group/field-group.css +226 -0
- package/src/field-group/field-group.tokens.json +33 -0
- package/src/filter-bar/filter-bar.css +1306 -0
- package/src/filter-bar/filter-bar.tokens.json +78 -0
- package/src/form/form.css +218 -0
- package/src/form/form.tokens.json +32 -0
- package/src/fullscreen/fullscreen.css +376 -0
- package/src/fullscreen/fullscreen.tokens.json +50 -0
- package/src/grid/grid.css +173 -0
- package/src/grid/grid.tokens.json +20 -0
- package/src/highlight/highlight.css +135 -0
- package/src/highlight/highlight.tokens.json +16 -0
- package/src/hover-card/hover-card.css +93 -0
- package/src/hover-card/hover-card.tokens.json +17 -0
- package/src/icon/icon.css +136 -0
- package/src/icon/icon.tokens.json +14 -0
- package/src/image/image.css +449 -0
- package/src/image/image.tokens.json +27 -0
- package/src/import-export-center/import-export-center.css +1175 -0
- package/src/import-export-center/import-export-center.tokens.json +84 -0
- package/src/inline/inline.css +149 -0
- package/src/inline/inline.tokens.json +14 -0
- package/src/inline-message/inline-message.css +396 -0
- package/src/inline-message/inline-message.tokens.json +19 -0
- package/src/kbd/kbd.css +490 -0
- package/src/kbd/kbd.tokens.json +40 -0
- package/src/link/link.css +237 -0
- package/src/link/link.tokens.json +25 -0
- package/src/list/list.css +240 -0
- package/src/list/list.tokens.json +42 -0
- package/src/list-item/list-item.css +937 -0
- package/src/list-item/list-item.tokens.json +114 -0
- package/src/listbox/listbox.css +674 -0
- package/src/listbox/listbox.tokens.json +72 -0
- package/src/loading-overlay/loading-overlay.css +730 -0
- package/src/loading-overlay/loading-overlay.tokens.json +48 -0
- package/src/markdown/markdown.css +168 -0
- package/src/markdown/markdown.tokens.json +18 -0
- package/src/menu/menu.css +817 -0
- package/src/menu/menu.tokens.json +78 -0
- package/src/menubar/menubar.css +170 -0
- package/src/menubar/menubar.tokens.json +32 -0
- package/src/message/message.css +1453 -0
- package/src/message/message.tokens.json +85 -0
- package/src/message-scroller/message-scroller.css +371 -0
- package/src/message-scroller/message-scroller.tokens.json +47 -0
- package/src/multi-select/multi-select.css +1427 -0
- package/src/multi-select/multi-select.tokens.json +114 -0
- package/src/navigation-menu/navigation-menu.css +1029 -0
- package/src/navigation-menu/navigation-menu.tokens.json +70 -0
- package/src/notification-center/notification-center.css +856 -0
- package/src/notification-center/notification-center.tokens.json +84 -0
- package/src/number-field/number-field.css +794 -0
- package/src/number-field/number-field.tokens.json +80 -0
- package/src/otp-input/otp-input.css +525 -0
- package/src/otp-input/otp-input.tokens.json +69 -0
- package/src/page/page.css +179 -0
- package/src/page/page.tokens.json +20 -0
- package/src/page-header/page-header.css +1181 -0
- package/src/page-header/page-header.tokens.json +71 -0
- package/src/pagination/pagination.css +500 -0
- package/src/pagination/pagination.tokens.json +56 -0
- package/src/popover/popover.css +526 -0
- package/src/popover/popover.tokens.json +60 -0
- package/src/progress-indicator/progress-indicator.css +1584 -0
- package/src/progress-indicator/progress-indicator.tokens.json +50 -0
- package/src/prompt-composer/prompt-composer.css +680 -0
- package/src/prompt-composer/prompt-composer.tokens.json +99 -0
- package/src/prose/prose.css +733 -0
- package/src/prose/prose.tokens.json +79 -0
- package/src/radio-group/radio-group.css +778 -0
- package/src/radio-group/radio-group.tokens.json +103 -0
- package/src/rating/rating.css +1071 -0
- package/src/rating/rating.tokens.json +88 -0
- package/src/result-state/result-state.css +980 -0
- package/src/result-state/result-state.tokens.json +63 -0
- package/src/runtime-properties.mjs +321 -0
- package/src/saved-views/saved-views.css +394 -0
- package/src/saved-views/saved-views.tokens.json +41 -0
- package/src/scroll-area/scroll-area.css +481 -0
- package/src/scroll-area/scroll-area.tokens.json +15 -0
- package/src/section/section.css +177 -0
- package/src/section/section.tokens.json +39 -0
- package/src/segmented-control/segmented-control.css +564 -0
- package/src/segmented-control/segmented-control.tokens.json +67 -0
- package/src/select/select.css +1593 -0
- package/src/select/select.tokens.json +110 -0
- package/src/side-navigation/side-navigation.css +1096 -0
- package/src/side-navigation/side-navigation.tokens.json +87 -0
- package/src/skeleton/skeleton.css +431 -0
- package/src/skeleton/skeleton.tokens.json +29 -0
- package/src/slider/slider.css +792 -0
- package/src/slider/slider.tokens.json +75 -0
- package/src/sparkline/sparkline.css +282 -0
- package/src/sparkline/sparkline.tokens.json +45 -0
- package/src/spinner/spinner.css +224 -0
- package/src/spinner/spinner.tokens.json +8 -0
- package/src/split-button/split-button.css +253 -0
- package/src/split-button/split-button.tokens.json +12 -0
- package/src/splitter/splitter.css +336 -0
- package/src/splitter/splitter.tokens.json +24 -0
- package/src/stack/stack.css +128 -0
- package/src/stack/stack.tokens.json +14 -0
- package/src/stat-card/stat-card.css +641 -0
- package/src/stat-card/stat-card.tokens.json +95 -0
- package/src/status-indicator/status-indicator.css +481 -0
- package/src/status-indicator/status-indicator.tokens.json +38 -0
- package/src/stepper/stepper.css +629 -0
- package/src/stepper/stepper.tokens.json +72 -0
- package/src/switch/switch.css +703 -0
- package/src/switch/switch.tokens.json +75 -0
- package/src/table/table.css +1482 -0
- package/src/table/table.tokens.json +69 -0
- package/src/table-of-contents/table-of-contents.css +533 -0
- package/src/table-of-contents/table-of-contents.tokens.json +53 -0
- package/src/tabs/tabs.css +998 -0
- package/src/tabs/tabs.tokens.json +82 -0
- package/src/tag/tag.css +768 -0
- package/src/tag/tag.tokens.json +85 -0
- package/src/tags-input/tags-input.css +757 -0
- package/src/tags-input/tags-input.tokens.json +84 -0
- package/src/text-field/text-field.css +900 -0
- package/src/text-field/text-field.tokens.json +87 -0
- package/src/textarea/textarea.css +365 -0
- package/src/textarea/textarea.tokens.json +59 -0
- package/src/time-field/time-field.css +719 -0
- package/src/time-field/time-field.tokens.json +86 -0
- package/src/time-picker/time-picker.css +665 -0
- package/src/time-picker/time-picker.tokens.json +66 -0
- package/src/time-range-picker/time-range-picker.css +1287 -0
- package/src/time-range-picker/time-range-picker.tokens.json +134 -0
- package/src/timeline/timeline.css +1596 -0
- package/src/timeline/timeline.tokens.json +96 -0
- package/src/toast/toast.css +943 -0
- package/src/toast/toast.tokens.json +90 -0
- package/src/toggle/toggle.css +290 -0
- package/src/toggle/toggle.tokens.json +54 -0
- package/src/toggle-group/toggle-group.css +463 -0
- package/src/toggle-group/toggle-group.tokens.json +54 -0
- package/src/toolbar/toolbar.css +434 -0
- package/src/toolbar/toolbar.tokens.json +49 -0
- package/src/tooltip/tooltip.css +368 -0
- package/src/tooltip/tooltip.tokens.json +39 -0
- package/src/top-navigation/top-navigation.css +1287 -0
- package/src/top-navigation/top-navigation.tokens.json +91 -0
- package/src/tour/tour.css +680 -0
- package/src/tour/tour.tokens.json +67 -0
- package/src/transfer/transfer.css +571 -0
- package/src/transfer/transfer.tokens.json +49 -0
- package/src/tree/tree.css +1000 -0
- package/src/tree/tree.tokens.json +80 -0
- package/src/typography/typography.css +211 -0
- package/src/typography/typography.tokens.json +31 -0
- package/src/upload/upload.css +1706 -0
- package/src/upload/upload.tokens.json +123 -0
- package/src/visually-hidden/visually-hidden.css +52 -0
- package/src/visually-hidden/visually-hidden.tokens.json +1 -0
- package/src/wizard/wizard.css +740 -0
- package/src/wizard/wizard.tokens.json +63 -0
|
@@ -0,0 +1,1593 @@
|
|
|
1
|
+
@layer slotted.components {
|
|
2
|
+
/**
|
|
3
|
+
* ── A control that shows one choice and opens a list of the rest ────────
|
|
4
|
+
*
|
|
5
|
+
* The family is two subtrees, and that is the first thing to know about this
|
|
6
|
+
* sheet. The **control** sits where the consumer wrote it, inside a field,
|
|
7
|
+
* inside a form. The **popup** is portalled to the end of the document so it
|
|
8
|
+
* escapes every clipping ancestor between the two. Nothing selects one from
|
|
9
|
+
* the other, so `data-ui='select'` is written on both roots — the contract
|
|
10
|
+
* records it under `portalledIdentity` — and every rule below states which of
|
|
11
|
+
* the two it is about through its own `data-part`.
|
|
12
|
+
*
|
|
13
|
+
* ┌─ root ─ the control ───────────────────────────────────────┐
|
|
14
|
+
* │ ┌─ trigger ─ role="combobox" ─ fills the frame ─────────┐ │
|
|
15
|
+
* │ │ value ▸ "Blueberry" │ │
|
|
16
|
+
* │ └───────────────────────────────────────────────────────┘ │
|
|
17
|
+
* │ clear ✕ arrow ⌄ │
|
|
18
|
+
* └────────────────────────────────────────────────────────────┘
|
|
19
|
+
*
|
|
20
|
+
* ┌─ positioner ─ the layer ───────────────────────────────────┐
|
|
21
|
+
* │ ┌─ content ─ role="listbox" ─ scrolls ───────────────┐ │
|
|
22
|
+
* │ │ group ▸ group-label │ │
|
|
23
|
+
* │ │ item ▸ icon │ label │ indicator │ │
|
|
24
|
+
* │ │ description │ │
|
|
25
|
+
* │ │ separator · empty · status │ │
|
|
26
|
+
* │ └────────────────────────────────────────────────────┘ │
|
|
27
|
+
* └────────────────────────────────────────────────────────────┘
|
|
28
|
+
*
|
|
29
|
+
* Under `search` the popup gains one row and the list moves one element in,
|
|
30
|
+
* because a text field is not an option and a listbox holds nothing else:
|
|
31
|
+
*
|
|
32
|
+
* ┌─ content ─ role="dialog" ─ data-search ────────────┐
|
|
33
|
+
* │ search ▸ search-icon │ search-input │
|
|
34
|
+
* │ ┌─ list ─ role="listbox" ─ scrolls ───────────┐ │
|
|
35
|
+
* │ │ item ▸ icon │ label ▸ match │ indicator │ │
|
|
36
|
+
* │ └──────────────────────────────────────────────┘ │
|
|
37
|
+
* └────────────────────────────────────────────────────┘
|
|
38
|
+
*
|
|
39
|
+
* Every part above `list` exists only under `search`, so a consumer who does
|
|
40
|
+
* not ask for it renders the DOM they rendered before it existed.
|
|
41
|
+
*
|
|
42
|
+
* ── Why the frame is `text-field`'s frame ────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* The border, the radius, the resting shadow, the focus ring, the invalid
|
|
45
|
+
* retargeting and the disabled treatment are the same declarations reading
|
|
46
|
+
* the same tokens. A form holding a text field, a number field and a select
|
|
47
|
+
* is one row of controls or it is three libraries, and none of the eight
|
|
48
|
+
* implementations surveyed for this family has an opinion here because none
|
|
49
|
+
* of them ships the other two.
|
|
50
|
+
*
|
|
51
|
+
* The trigger is a borderless button filling that frame, so the frame draws
|
|
52
|
+
* the ring for it through `:has()` — the rule `text-field` already writes for
|
|
53
|
+
* its input — and `clear` is a sibling of the trigger rather than a child of
|
|
54
|
+
* it, because a focusable descendant of a `combobox` is a widget inside a
|
|
55
|
+
* widget. The chevron and the clear button then sit on top of the trigger
|
|
56
|
+
* rather than beside it, for the reason written above the trigger's own rule.
|
|
57
|
+
*
|
|
58
|
+
* ── Why the popup is a grid and the item is a subgrid ────────────────
|
|
59
|
+
*
|
|
60
|
+
* The same architecture `menu` uses, and for the same reason: the surface
|
|
61
|
+
* owns three columns — the icon, the label, the check — and every item,
|
|
62
|
+
* group and group label passes through them with `grid-template-columns:
|
|
63
|
+
* subgrid`, so labels line up across the whole list however many of the
|
|
64
|
+
* options carry artwork.
|
|
65
|
+
*
|
|
66
|
+
* The check sits in the **last** column rather than sharing the first with
|
|
67
|
+
* the icon, which is where this family parts company with `menu`. A menu's
|
|
68
|
+
* check *is* its item's icon — an option's is not: a country's flag and the
|
|
69
|
+
* mark saying it is chosen are two different facts about one row, and a
|
|
70
|
+
* reader who can see only one of them at a time cannot scan the list. The
|
|
71
|
+
* columns carry no `column-gap`, so a list with neither artwork nor a check
|
|
72
|
+
* collapses both gutters to nothing.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/* ── The control ──────────────────────────────────────────────────────── */
|
|
76
|
+
|
|
77
|
+
[data-ui='select'][data-part='root'] {
|
|
78
|
+
/**
|
|
79
|
+
* Type, height, inline padding, gap and glyph size come from the control
|
|
80
|
+
* scales — the only ones the theme restates per density — so a select in a
|
|
81
|
+
* dense table shrinks whole rather than in parts.
|
|
82
|
+
*/
|
|
83
|
+
--_font-size: var(--slotted-control-font-size-md, 13px);
|
|
84
|
+
--_height: var(--slotted-control-height-md, 34px);
|
|
85
|
+
--_padding-inline: var(--slotted-control-padding-inline-md, 13px);
|
|
86
|
+
--_gap: var(--slotted-control-gap-md, 8px);
|
|
87
|
+
/** The floor under the glyph scale at the dense step (SL3); a family token so a theme can retune it. */
|
|
88
|
+
--_icon-floor: var(--slotted-select-icon-size-min, 14px);
|
|
89
|
+
--_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
|
|
90
|
+
--_inner-size: var(--slotted-control-inner-size-md, 24px);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The drawn clear button, sized by its glyph and capped at the inner size
|
|
94
|
+
* of a control at this step (`--slotted-control-inner-size-*`). The
|
|
95
|
+
* arithmetic is `text-field`'s, because the two buttons are the same button
|
|
96
|
+
* in the same frame and a second answer would drift from the first.
|
|
97
|
+
*/
|
|
98
|
+
--_action-size: var(
|
|
99
|
+
--slotted-select-action-size,
|
|
100
|
+
min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
align-items: center;
|
|
104
|
+
background-color: var(--slotted-select-background, var(--slotted-control-surface, Field));
|
|
105
|
+
border: var(--slotted-control-border-width, 1px) solid
|
|
106
|
+
var(--slotted-select-border-color, var(--slotted-control-border-color, ButtonBorder));
|
|
107
|
+
border-radius: var(--slotted-select-radius, var(--slotted-control-radius, 4px));
|
|
108
|
+
box-shadow: var(--slotted-select-shadow, var(--slotted-control-shadow, none));
|
|
109
|
+
box-sizing: border-box;
|
|
110
|
+
color: var(--slotted-select-color, var(--slotted-text, CanvasText));
|
|
111
|
+
column-gap: var(--_gap);
|
|
112
|
+
/**
|
|
113
|
+
* Two lanes on one row: what the control holds, and what ends it (SL9).
|
|
114
|
+
*
|
|
115
|
+
* The chips used to be a row hanging over the trigger's content box, with
|
|
116
|
+
* its inline end written as the same arithmetic the chevron and the clear
|
|
117
|
+
* button are placed by. A row that hangs reserves no space, so the lane it
|
|
118
|
+
* may use was a number kept in step by hand, and six chosen values ran
|
|
119
|
+
* under the two controls that end the control. Here the lane *is* a track:
|
|
120
|
+
* it ends where the next track begins, and no arithmetic can be out of
|
|
121
|
+
* date. The trigger spans both, so it is still the ground every press that
|
|
122
|
+
* is not a chip or the clear button lands on.
|
|
123
|
+
*/
|
|
124
|
+
display: grid;
|
|
125
|
+
font-family: var(--slotted-control-font-family, system-ui, sans-serif);
|
|
126
|
+
font-size: var(--_font-size);
|
|
127
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
128
|
+
inline-size: 100%;
|
|
129
|
+
/* A control in a grid track narrower than its intrinsic width would
|
|
130
|
+
otherwise refuse to shrink and take the row's edge with it. */
|
|
131
|
+
min-inline-size: 0;
|
|
132
|
+
/* A minimum rather than a height: the frame is the taller of the control
|
|
133
|
+
scale and whatever a consumer put in the value. */
|
|
134
|
+
min-block-size: var(--_height);
|
|
135
|
+
/* The containing block of the chevron and the clear button, both of which
|
|
136
|
+
are placed against the frame's inline end rather than laid out in it. */
|
|
137
|
+
position: relative;
|
|
138
|
+
transition:
|
|
139
|
+
background-color var(--slotted-control-transition-duration)
|
|
140
|
+
var(--slotted-control-transition-easing),
|
|
141
|
+
border-color var(--slotted-control-transition-duration)
|
|
142
|
+
var(--slotted-control-transition-easing),
|
|
143
|
+
box-shadow var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* And the clear button's own lane, before the chevron's, when there is one. */
|
|
147
|
+
[data-ui='select'][data-part='root']:has(> [data-part='clear']) {
|
|
148
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A select that does not fill its field. The frame shrinks to its widest
|
|
153
|
+
* value, which is what a filter beside a table needs and what a field never
|
|
154
|
+
* does — hence the default the contract states, `fullWidth: true`.
|
|
155
|
+
*/
|
|
156
|
+
[data-ui='select'][data-part='root']:not([data-full-width]) {
|
|
157
|
+
inline-size: auto;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The control. It gives up its own frame entirely — border, ground, ring and
|
|
162
|
+
* radius all belong to the box around it — and keeps what makes it a button:
|
|
163
|
+
* the press, the `Enter` and `Space` the platform already handles, and every
|
|
164
|
+
* native attribute the consumer wrote.
|
|
165
|
+
*
|
|
166
|
+
* **It fills the frame, and the two marks sit on top of it.** The chevron
|
|
167
|
+
* belongs at the very end of the control — it is the affordance for the whole
|
|
168
|
+
* thing, which is where a native `select` draws it and where every surveyed
|
|
169
|
+
* implementation puts it — and the clear button belongs before it. In document
|
|
170
|
+
* order the clear button is a sibling of the trigger, so a chevron inside the
|
|
171
|
+
* trigger can only ever be drawn *before* the clear button, and the first
|
|
172
|
+
* capture of this family showed exactly that: `Blueberry ⌄ ✕`.
|
|
173
|
+
*
|
|
174
|
+
* So the chevron and the clear button stand in their own lane at the frame's
|
|
175
|
+
* inline end, and the trigger spans every lane and reserves their width as
|
|
176
|
+
* its own end padding. Padding is part of the trigger's box, so a press on
|
|
177
|
+
* the chevron still lands on the button that opens the popup — which is why
|
|
178
|
+
* the chevron can be inert and the arrangement costs nothing.
|
|
179
|
+
*/
|
|
180
|
+
[data-ui='select'] > [data-part='trigger'] {
|
|
181
|
+
align-items: center;
|
|
182
|
+
appearance: none;
|
|
183
|
+
background-color: transparent;
|
|
184
|
+
border: 0;
|
|
185
|
+
box-sizing: border-box;
|
|
186
|
+
color: inherit;
|
|
187
|
+
/**
|
|
188
|
+
* The hand, as `button/button.css` draws it on every button in the
|
|
189
|
+
* library. This drew an arrow until SL11, and the arrow came from the
|
|
190
|
+
* native `select` — the element this family refused, on the grounds that
|
|
191
|
+
* the operating system draws its popup and a web surface is not what it
|
|
192
|
+
* gets. Keeping the native cursor after refusing the native element left
|
|
193
|
+
* the one control in a form that says nothing happens when it is pressed.
|
|
194
|
+
*/
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
display: flex;
|
|
197
|
+
font: inherit;
|
|
198
|
+
gap: var(--_gap);
|
|
199
|
+
/* The ground: every lane on the one row, so nothing the reader presses
|
|
200
|
+
between the chips and the border falls past the button. */
|
|
201
|
+
grid-area: 1 / 1 / 2 / -1;
|
|
202
|
+
/* The frame's border box, not its content box: on a coarse pointer the
|
|
203
|
+
frame is the touch target's height and the trigger is too. */
|
|
204
|
+
margin-block: calc(var(--slotted-control-border-width, 1px) * -1);
|
|
205
|
+
min-block-size: var(--_height);
|
|
206
|
+
/* An `auto` minimum refuses to shrink below the longest label, which is how
|
|
207
|
+
a select in a narrow column pushes its own field off the row. */
|
|
208
|
+
min-inline-size: 0;
|
|
209
|
+
/* The frame draws the ring for it, so two rings never nest. */
|
|
210
|
+
outline: none;
|
|
211
|
+
padding-block: 0;
|
|
212
|
+
padding-inline-start: var(--_padding-inline);
|
|
213
|
+
/* The inset, the chevron and the gap before it. */
|
|
214
|
+
padding-inline-end: calc(var(--_padding-inline) + var(--_icon-size) + var(--_gap));
|
|
215
|
+
text-align: start;
|
|
216
|
+
touch-action: manipulation;
|
|
217
|
+
-webkit-tap-highlight-color: transparent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* And the clear button's own box, plus a second gap, when there is one. */
|
|
221
|
+
[data-ui='select'][data-part='root']:has(> [data-part='clear']) > [data-part='trigger'] {
|
|
222
|
+
padding-inline-end: calc(
|
|
223
|
+
var(--_padding-inline) + var(--_icon-size) + var(--_gap) + var(--_action-size) + var(--_gap)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* One line, ending in an ellipsis. A value that wrapped would change the
|
|
229
|
+
* control's height as the reader chose, and a form that reflows on selection
|
|
230
|
+
* is a form that moves under the pointer.
|
|
231
|
+
*/
|
|
232
|
+
[data-ui='select'] [data-part='value'] {
|
|
233
|
+
flex: 1 1 auto;
|
|
234
|
+
min-inline-size: 0;
|
|
235
|
+
overflow: hidden;
|
|
236
|
+
text-overflow: ellipsis;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Nothing chosen yet. The same weight the reader will see once they choose,
|
|
242
|
+
* one step quieter, so the control does not change shape when it fills.
|
|
243
|
+
*/
|
|
244
|
+
[data-ui='select'] [data-part='value'][data-placeholder] {
|
|
245
|
+
color: var(--slotted-select-placeholder-color, var(--slotted-text-muted, GrayText));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A value the collection could not resolve to a label, which is what an
|
|
250
|
+
* asynchronous list holds for one render. It is not the placeholder — the
|
|
251
|
+
* control has a value — and it is not a normal label either, so it is set in
|
|
252
|
+
* the same face and marked as unresolved for a stylesheet that wants to say
|
|
253
|
+
* more.
|
|
254
|
+
*/
|
|
255
|
+
[data-ui='select'] [data-part='value'][data-unmatched] {
|
|
256
|
+
font-variant-numeric: tabular-nums;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── Several chosen values, as the chips they are ────────────────────── */
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The preview of a multiple selection (SL4). It was a sentence about the
|
|
263
|
+
* choice — "Apple +1" — and it is now the choice: one `tag` per value, each
|
|
264
|
+
* with the remove control a tag already has, and one more chip counting what
|
|
265
|
+
* did not fit.
|
|
266
|
+
*
|
|
267
|
+
* The chips are `tag`'s, worn as `data-ui` beside this row, so the chip a
|
|
268
|
+
* reader removes here is the chip they know from everywhere else and its
|
|
269
|
+
* geometry is stated once. Nothing about a chip is decided here: the row
|
|
270
|
+
* asks for the small step and the chip answers with its own arithmetic,
|
|
271
|
+
* floored at the target minimum because its remove control is a control.
|
|
272
|
+
*
|
|
273
|
+
* **The row is a sibling of the trigger, and a lane of the control** (SL9).
|
|
274
|
+
* The trigger is a `button`, and a button cannot contain a remove control:
|
|
275
|
+
* that is a control inside a control, which no browser and no reader resolves
|
|
276
|
+
* the same way. So the row is written beside the button rather than in it —
|
|
277
|
+
* and it takes the first track of the frame's row, so the space it needs is
|
|
278
|
+
* space the control gives it and the chips end where the lane of the final
|
|
279
|
+
* controls begins. It lets every press it does not want through to the
|
|
280
|
+
* trigger spanning underneath, so the frame still opens the popup everywhere
|
|
281
|
+
* a chip is not.
|
|
282
|
+
*/
|
|
283
|
+
[data-ui='select'][data-part='root'] > [data-part='tags'] {
|
|
284
|
+
align-items: center;
|
|
285
|
+
display: flex;
|
|
286
|
+
gap: var(--slotted-space-2, 0.25rem);
|
|
287
|
+
/* The content lane: the chips share it with the trigger's own text. */
|
|
288
|
+
grid-area: 1 / 1 / 2 / 2;
|
|
289
|
+
/* The gutter the value begins from, so a chip starts where a label would. */
|
|
290
|
+
margin-inline-start: var(--_padding-inline);
|
|
291
|
+
/**
|
|
292
|
+
* The row is at least a target tall, because a chip's remove control takes
|
|
293
|
+
* the rest of its own target from a transparent halo that stands outside
|
|
294
|
+
* the chip (TG2), and the clip below would otherwise cut that halo off at
|
|
295
|
+
* the chip's edge — the press area measured 24 × 22.8 inside the trigger
|
|
296
|
+
* and 24 × 24 everywhere else. The chips stay centred in it. It is never
|
|
297
|
+
* taller than the control's own inner box, so the height of the frame is
|
|
298
|
+
* the height of a select that chose one thing.
|
|
299
|
+
*/
|
|
300
|
+
min-block-size: var(--slotted-target-min, 24px);
|
|
301
|
+
/* A lane a chip cannot widen: the track is `minmax(0, 1fr)` and this is
|
|
302
|
+
what makes a chip shrink against it rather than push it. */
|
|
303
|
+
min-inline-size: 0;
|
|
304
|
+
/* A chip that does not fit is hidden by the collapse, not clipped; this is
|
|
305
|
+
the guard for the frame between one measurement and the next. */
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
/* The gaps between chips belong to the trigger underneath. */
|
|
308
|
+
pointer-events: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
[data-ui='select'] > [data-part='tags'] > * {
|
|
312
|
+
/* Each chip takes its own presses back, and stands above the trigger that
|
|
313
|
+
spans the lane underneath it. */
|
|
314
|
+
pointer-events: auto;
|
|
315
|
+
position: relative;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The chip that counts what did not fit. It is a chip and not a number
|
|
320
|
+
* beside them, so the row reads as one run of chips whose last one happens
|
|
321
|
+
* to be a count, and `createCollapse` can measure it the way it measures
|
|
322
|
+
* every other unit.
|
|
323
|
+
*/
|
|
324
|
+
[data-ui='select'] > [data-part='tags'] > [data-overflow] {
|
|
325
|
+
flex: none;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The sentence, for a control too narrow for a chip. It lives inside the
|
|
330
|
+
* value, so the trigger is named by it whichever of the two is drawn, and it
|
|
331
|
+
* is taken out of sight rather than out of the tree while the chips are
|
|
332
|
+
* shown: a button whose accessible name disappeared when the frame grew is a
|
|
333
|
+
* button that changes what it is called when the window is resized.
|
|
334
|
+
*/
|
|
335
|
+
[data-ui='select'] [data-part='summary'] {
|
|
336
|
+
display: block;
|
|
337
|
+
overflow: hidden;
|
|
338
|
+
text-overflow: ellipsis;
|
|
339
|
+
/* Its own bidi run, ordered by its own content, so a Latin count in a
|
|
340
|
+
right-to-left row is clipped at its own end rather than at the page's —
|
|
341
|
+
the repair `status` and `empty` already make, and for the same reason. */
|
|
342
|
+
unicode-bidi: plaintext;
|
|
343
|
+
white-space: nowrap;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
[data-ui='select'][data-part='root']:not([data-narrow]) [data-part='summary'] {
|
|
347
|
+
block-size: 1px;
|
|
348
|
+
clip-path: inset(50%);
|
|
349
|
+
inline-size: 1px;
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
white-space: nowrap;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
[data-ui='select'][data-part='root'][data-narrow] > [data-part='tags'] {
|
|
355
|
+
display: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The chevron. The library's own `Glyph` — an `<svg>` the binding draws
|
|
360
|
+
* inside an `icon` box — rather than a character, and the box turns over when
|
|
361
|
+
* the popup opens: the one movement in the control, and the only thing on the
|
|
362
|
+
* closed control that says it opens at all.
|
|
363
|
+
*/
|
|
364
|
+
[data-ui='select'] > [data-part='arrow'] {
|
|
365
|
+
--slotted-icon-size: var(--_icon-size);
|
|
366
|
+
|
|
367
|
+
align-items: center;
|
|
368
|
+
block-size: var(--_icon-size);
|
|
369
|
+
color: var(--slotted-select-arrow-color, var(--slotted-text-muted, GrayText));
|
|
370
|
+
display: flex;
|
|
371
|
+
/* The last lane, whichever of the two arrangements is in force. */
|
|
372
|
+
grid-area: 1 / -2 / 2 / -1;
|
|
373
|
+
inline-size: var(--_icon-size);
|
|
374
|
+
justify-content: center;
|
|
375
|
+
margin-inline-end: var(--_padding-inline);
|
|
376
|
+
/* Inert on purpose: the trigger spans this lane underneath it, so a press
|
|
377
|
+
here opens the popup rather than landing on a decorative span. */
|
|
378
|
+
pointer-events: none;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
[data-ui='select'] > [data-part='arrow'] > [data-ui='icon'] {
|
|
382
|
+
transition: rotate var(--slotted-control-transition-duration)
|
|
383
|
+
var(--slotted-control-transition-easing);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
[data-ui='select'][data-open] > [data-part='arrow'] > [data-ui='icon'] {
|
|
387
|
+
rotate: 180deg;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The ring, where the chevron stands. While an answer is travelling the
|
|
392
|
+
* control shows the ring instead of the chevron: the chevron says the popup
|
|
393
|
+
* opens, and it does not open onto anything yet (SL6).
|
|
394
|
+
*
|
|
395
|
+
* The arc is not drawn here. It is the one `spinner/spinner.css` draws for
|
|
396
|
+
* every family that waits — B4 extracted it for exactly this second host —
|
|
397
|
+
* and the element wears `data-ui='spinner'` beside this family's own part.
|
|
398
|
+
* All this rule does is stand it where the chevron stood and give it the
|
|
399
|
+
* chevron's size and ink, through the size token the shared sheet reads.
|
|
400
|
+
*/
|
|
401
|
+
[data-ui='select'] > [data-part='busy'] {
|
|
402
|
+
--slotted-spinner-size: var(--_icon-size);
|
|
403
|
+
|
|
404
|
+
color: var(--slotted-select-arrow-color, var(--slotted-text-muted, GrayText));
|
|
405
|
+
/* The chevron's lane, because it stands where the chevron stood. */
|
|
406
|
+
grid-area: 1 / -2 / 2 / -1;
|
|
407
|
+
margin-inline-end: var(--_padding-inline);
|
|
408
|
+
/* Inert on purpose, as the chevron is: the trigger spans this lane
|
|
409
|
+
underneath it, so a press here opens the popup. */
|
|
410
|
+
pointer-events: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The clear button, in the lane before the chevron's. Its box is wider than
|
|
415
|
+
* its glyph by the target it recovers, and that overhang is taken back on
|
|
416
|
+
* both sides — so every gap in the lane is measured between glyphs and the
|
|
417
|
+
* *glyph* keeps the rhythm the value begins with. `text-field`'s arithmetic,
|
|
418
|
+
* stated in the terms a track states it.
|
|
419
|
+
*/
|
|
420
|
+
[data-ui='select'] > [data-part='clear'] {
|
|
421
|
+
align-items: center;
|
|
422
|
+
appearance: none;
|
|
423
|
+
background-color: transparent;
|
|
424
|
+
block-size: var(--_action-size);
|
|
425
|
+
border: 0;
|
|
426
|
+
border-radius: var(--slotted-select-radius, var(--slotted-control-radius, 4px));
|
|
427
|
+
color: var(--slotted-select-action-color, var(--slotted-text-muted, GrayText));
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
display: inline-flex;
|
|
430
|
+
grid-area: 1 / 2 / 2 / 3;
|
|
431
|
+
inline-size: var(--_action-size);
|
|
432
|
+
justify-content: center;
|
|
433
|
+
margin-inline: calc((var(--_icon-size) - var(--_action-size)) / 2);
|
|
434
|
+
/* The user agent pads a button, and inside a box sized to its glyph that
|
|
435
|
+
padding squeezed the glyph to twelve pixels. */
|
|
436
|
+
padding: 0;
|
|
437
|
+
/* Over the trigger that spans this lane, so it takes its own presses back
|
|
438
|
+
from the button running underneath it. */
|
|
439
|
+
position: relative;
|
|
440
|
+
touch-action: manipulation;
|
|
441
|
+
transition:
|
|
442
|
+
background-color var(--slotted-control-transition-duration)
|
|
443
|
+
var(--slotted-control-transition-easing),
|
|
444
|
+
color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
|
|
445
|
+
-webkit-tap-highlight-color: transparent;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The hit area, recovered outside the paint. At `sm` with `dense` the whole
|
|
450
|
+
* control is 24 pixels tall — the WCAG 2.2 minimum — so nothing drawn inside
|
|
451
|
+
* it can also be 24. This grows the target back in both axes and leaves a
|
|
452
|
+
* larger one alone.
|
|
453
|
+
*/
|
|
454
|
+
[data-ui='select'] > [data-part='clear']::after {
|
|
455
|
+
content: '';
|
|
456
|
+
inset: min(0px, calc((100% - var(--slotted-target-min, 24px)) / 2));
|
|
457
|
+
position: absolute;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* On a finger the target is the comfortable size in both axes and the box
|
|
461
|
+
is the one a mouse sees: the ground and the focus ring stay square, and
|
|
462
|
+
the hit area above grows to 44 pixels around them. It does not reach into
|
|
463
|
+
the chevron's lane after it, which is the trigger's to answer. */
|
|
464
|
+
@media (pointer: coarse) {
|
|
465
|
+
[data-ui='select'] > [data-part='clear']::after {
|
|
466
|
+
inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
|
|
467
|
+
inset-inline-end: 0;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* The cross is a `Glyph` the binding draws, at the chevron's size. */
|
|
472
|
+
[data-ui='select'] > [data-part='clear'] {
|
|
473
|
+
--slotted-icon-size: var(--_icon-size);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* ── Size ─────────────────────────────────────────────────────────────
|
|
477
|
+
*
|
|
478
|
+
* Three steps, matching the field the control sits in and the popup it
|
|
479
|
+
* opens. The control reads the field's step rather than its own default when
|
|
480
|
+
* there is a field, because a `sm` field holding an `md` control is a field
|
|
481
|
+
* and a control disagreeing about one row.
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
[data-ui='select'][data-part='root'][data-size='sm'] {
|
|
485
|
+
--_font-size: var(--slotted-control-font-size-sm, 12px);
|
|
486
|
+
--_height: var(--slotted-control-height-sm, 30px);
|
|
487
|
+
--_padding-inline: var(--slotted-control-padding-inline-sm, 10px);
|
|
488
|
+
--_gap: var(--slotted-control-gap-sm, 6px);
|
|
489
|
+
--_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-sm, 14px));
|
|
490
|
+
--_inner-size: var(--slotted-control-inner-size-sm, 22px);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
[data-ui='select'][data-part='root'][data-size='md'] {
|
|
494
|
+
--_font-size: var(--slotted-control-font-size-md, 13px);
|
|
495
|
+
--_height: var(--slotted-control-height-md, 34px);
|
|
496
|
+
--_padding-inline: var(--slotted-control-padding-inline-md, 13px);
|
|
497
|
+
--_gap: var(--slotted-control-gap-md, 8px);
|
|
498
|
+
--_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
|
|
499
|
+
--_inner-size: var(--slotted-control-inner-size-md, 24px);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
[data-ui='select'][data-part='root'][data-size='lg'] {
|
|
503
|
+
--_font-size: var(--slotted-control-font-size-lg, 14px);
|
|
504
|
+
--_height: var(--slotted-control-height-lg, 38px);
|
|
505
|
+
--_padding-inline: var(--slotted-control-padding-inline-lg, 16px);
|
|
506
|
+
--_gap: var(--slotted-control-gap-lg, 8px);
|
|
507
|
+
--_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-lg, 18px));
|
|
508
|
+
--_inner-size: var(--slotted-control-inner-size-lg, 28px);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* ── Variant ──────────────────────────────────────────────────────────
|
|
512
|
+
*
|
|
513
|
+
* The three `text-field` ships, with the same values, because a form with a
|
|
514
|
+
* `subtle` text field and an `outline` select beside it is a form that looks
|
|
515
|
+
* unfinished. `plain` is what a dense table needs: a cell that is only a
|
|
516
|
+
* control while it is being used, keeping exactly the same box so the row
|
|
517
|
+
* does not reflow when the pointer crosses it.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
[data-ui='select'][data-part='root'][data-variant='subtle'] {
|
|
521
|
+
background-color: var(
|
|
522
|
+
--slotted-select-subtle-background,
|
|
523
|
+
var(--slotted-surface-sunken, Canvas)
|
|
524
|
+
);
|
|
525
|
+
border-color: var(
|
|
526
|
+
--slotted-select-subtle-border-color,
|
|
527
|
+
var(--slotted-border-subtle, ButtonBorder)
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* No edge, no lift, no ground and no corner of its own: the cell it sits in
|
|
532
|
+
is the box, the row is the ground, a cell has no corner, and the resting
|
|
533
|
+
shadow would draw the cell where there is none. */
|
|
534
|
+
[data-ui='select'][data-part='root'][data-variant='plain'] {
|
|
535
|
+
background-color: transparent;
|
|
536
|
+
border-color: transparent;
|
|
537
|
+
border-radius: var(--slotted-radius-none, 0px);
|
|
538
|
+
box-shadow: none;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The one rule the theme cannot write for the family. A transparent border
|
|
543
|
+
* has no system colour to be remapped to, so the `plain` control would lose
|
|
544
|
+
* its only edge exactly where an edge matters most.
|
|
545
|
+
*/
|
|
546
|
+
@media (forced-colors: active) {
|
|
547
|
+
[data-ui='select'][data-part='root'][data-variant='plain'] {
|
|
548
|
+
border-color: ButtonBorder;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* ── States on the control ────────────────────────────────────────────
|
|
553
|
+
*
|
|
554
|
+
* Written strongest last, each weaker one guarded against every stronger one
|
|
555
|
+
* it can meet, which is the order `STATE_PRECEDENCE` sets.
|
|
556
|
+
*/
|
|
557
|
+
|
|
558
|
+
[data-ui='select'][data-part='root']:hover:not([data-disabled]):not([data-readonly]):not(
|
|
559
|
+
[data-invalid]
|
|
560
|
+
) {
|
|
561
|
+
border-color: var(--slotted-select-hover-border-color, var(--slotted-tone-accent-border));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
[data-ui='select'][data-part='root'][data-variant='subtle']:hover:not([data-disabled]):not(
|
|
565
|
+
[data-readonly]
|
|
566
|
+
):not([data-invalid]) {
|
|
567
|
+
background-color: var(
|
|
568
|
+
--slotted-select-subtle-hover-background,
|
|
569
|
+
var(--slotted-tone-secondary-subtle-hover)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
[data-ui='select'][data-part='root'][data-variant='plain']:hover:not([data-disabled]):not(
|
|
574
|
+
[data-readonly]
|
|
575
|
+
):not([data-invalid]) {
|
|
576
|
+
background-color: var(
|
|
577
|
+
--slotted-select-plain-hover-background,
|
|
578
|
+
var(--slotted-tone-secondary-subtle-hover)
|
|
579
|
+
);
|
|
580
|
+
/* The accent edge the other variants take would draw the cell's outline. */
|
|
581
|
+
border-color: transparent;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The ring belongs to the frame and answers the trigger's focus. A button
|
|
586
|
+
* matches `:focus-visible` for a keyboard arrival and not for a pointer
|
|
587
|
+
* press, which is exactly right here: a reader who pressed the control can
|
|
588
|
+
* see where they pressed, and the popup is already open under it.
|
|
589
|
+
*/
|
|
590
|
+
[data-ui='select'][data-part='root']:has(> [data-part='trigger']:focus-visible) {
|
|
591
|
+
/* The resting shadow, recoloured: the geometry is the foundation's and the
|
|
592
|
+
ink is the ring's, so the lift and the outline are one colour. */
|
|
593
|
+
box-shadow: var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
|
|
594
|
+
var(--slotted-focus-ring-color, Highlight);
|
|
595
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
596
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The ring of a control whose box is not its own is drawn inside that box,
|
|
601
|
+
* the way the inner controls draw theirs inside the frame: two pixels past
|
|
602
|
+
* the control is two pixels over the next row's rule. No lift either — a
|
|
603
|
+
* shadow that appears only under focus is a cell that changes size when it
|
|
604
|
+
* is used.
|
|
605
|
+
*/
|
|
606
|
+
[data-ui='select'][data-part='root'][data-variant='plain']:has(
|
|
607
|
+
> [data-part='trigger']:focus-visible
|
|
608
|
+
) {
|
|
609
|
+
box-shadow: none;
|
|
610
|
+
outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The open control keeps the accent edge whether or not the keyboard opened
|
|
615
|
+
* it. A popup hanging off a control with a resting border reads as two
|
|
616
|
+
* elements that happen to be adjacent.
|
|
617
|
+
*/
|
|
618
|
+
[data-ui='select'][data-part='root'][data-open]:not([data-disabled]) {
|
|
619
|
+
border-color: var(--slotted-select-open-border-color, var(--slotted-tone-accent-border));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Invalid outranks hover and open, and is written after both: a control
|
|
624
|
+
* reporting an error should not stop looking wrong because it was opened.
|
|
625
|
+
* The ring is retargeted rather than repainted — every rule that draws focus
|
|
626
|
+
* reads `--slotted-focus-ring-color`, so pointing that one name at the
|
|
627
|
+
* invalid colour moves the outline and the shadow together.
|
|
628
|
+
*/
|
|
629
|
+
[data-ui='select'][data-part='root'][data-invalid] {
|
|
630
|
+
--slotted-focus-ring-color: var(
|
|
631
|
+
--slotted-select-invalid-focus-ring-color,
|
|
632
|
+
var(--slotted-focus-ring-color-invalid, Highlight)
|
|
633
|
+
);
|
|
634
|
+
--_invalid-edge-width: var(
|
|
635
|
+
--slotted-select-invalid-edge-width,
|
|
636
|
+
var(--slotted-control-invalid-edge-width, 1px)
|
|
637
|
+
);
|
|
638
|
+
--_invalid-edge-color: var(
|
|
639
|
+
--slotted-select-invalid-border-color,
|
|
640
|
+
var(--slotted-tone-danger-border)
|
|
641
|
+
);
|
|
642
|
+
--_invalid-edge: inset 0 0 0 var(--_invalid-edge-width) var(--_invalid-edge-color);
|
|
643
|
+
|
|
644
|
+
border-color: var(--slotted-select-invalid-border-color, var(--slotted-tone-danger-border));
|
|
645
|
+
/*
|
|
646
|
+
* Width as well as colour (WCAG 1.4.1), the edge `text-field` draws: in
|
|
647
|
+
* `high-contrast` the danger border is the resting edge's black. The line
|
|
648
|
+
* is a shadow inside the border, so neither box nor text moves, and it
|
|
649
|
+
* takes the resting lift's place.
|
|
650
|
+
*/
|
|
651
|
+
box-shadow: var(--_invalid-edge);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
[data-ui='select'][data-part='root'][data-invalid]:has(> [data-part='trigger']:focus-visible) {
|
|
655
|
+
box-shadow:
|
|
656
|
+
var(--_invalid-edge),
|
|
657
|
+
var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
|
|
658
|
+
var(--slotted-focus-ring-color, Highlight);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
[data-ui='select'][data-part='root'][data-variant='plain'][data-invalid]:has(
|
|
662
|
+
> [data-part='trigger']:focus-visible
|
|
663
|
+
) {
|
|
664
|
+
/* The ring lies inside the box here, over the border and an edge as wide as
|
|
665
|
+
it; the edge reaches past the ring. */
|
|
666
|
+
box-shadow: inset 0 0 0
|
|
667
|
+
calc(
|
|
668
|
+
var(--slotted-focus-ring-offset, 2px) - var(--slotted-control-border-width, 1px) +
|
|
669
|
+
var(--_invalid-edge-width)
|
|
670
|
+
)
|
|
671
|
+
var(--_invalid-edge-color);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/*
|
|
675
|
+
* Forced colours remove every box shadow, and the edge with them. The
|
|
676
|
+
* width comes back as an outline inside the border, and a focused invalid
|
|
677
|
+
* control draws its ring double, which a valid ring is not.
|
|
678
|
+
*/
|
|
679
|
+
@media (forced-colors: active) {
|
|
680
|
+
[data-ui='select'][data-part='root'][data-invalid] {
|
|
681
|
+
outline: var(--_invalid-edge-width) solid CanvasText;
|
|
682
|
+
outline-offset: calc(
|
|
683
|
+
-1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
[data-ui='select'][data-part='root'][data-invalid]:has(> [data-part='trigger']:focus-visible) {
|
|
688
|
+
outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
|
|
689
|
+
var(--slotted-focus-ring-color, Highlight);
|
|
690
|
+
outline-offset: var(--slotted-focus-ring-offset, 2px);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
[data-ui='select'][data-part='root'][data-variant='plain'][data-invalid]:has(
|
|
694
|
+
> [data-part='trigger']:focus-visible
|
|
695
|
+
) {
|
|
696
|
+
outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
|
|
697
|
+
var(--slotted-focus-ring-color, Highlight);
|
|
698
|
+
outline-offset: calc(
|
|
699
|
+
-1 * (var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width))
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Read-only stays focusable and legible and must not be mistakable for
|
|
706
|
+
* disabled: the value is still real and still submitted. The ground says
|
|
707
|
+
* "not yours to change" and the ink says nothing at all. Neither a button nor
|
|
708
|
+
* `role="combobox"` has a readonly the platform enforces, so the family
|
|
709
|
+
* refuses the change itself and this is what the refusal looks like.
|
|
710
|
+
*/
|
|
711
|
+
[data-ui='select'][data-part='root'][data-readonly] {
|
|
712
|
+
background-color: var(
|
|
713
|
+
--slotted-select-readonly-background,
|
|
714
|
+
var(--slotted-tone-secondary-subtle-hover)
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
[data-ui='select'][data-part='root'][data-readonly] [data-part='arrow'] {
|
|
719
|
+
opacity: 0.5;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* `:disabled` on the trigger is matched beside `data-disabled` on the frame,
|
|
724
|
+
* because a control can be disabled by something that never told it so: a
|
|
725
|
+
* disabled `fieldset` disables every button inside it, and the button has no
|
|
726
|
+
* way to say. The attribute alone left such a control looking operable and
|
|
727
|
+
* answering nothing.
|
|
728
|
+
*/
|
|
729
|
+
[data-ui='select'][data-part='root'][data-disabled],
|
|
730
|
+
[data-ui='select'][data-part='root']:has(> [data-part='trigger']:disabled) {
|
|
731
|
+
background-color: var(--slotted-select-disabled-background, var(--slotted-disabled-background));
|
|
732
|
+
border-color: var(--slotted-select-disabled-border-color, var(--slotted-disabled-border));
|
|
733
|
+
/* Nothing a person cannot reach is lifted off the page. */
|
|
734
|
+
box-shadow: none;
|
|
735
|
+
/* Muted to the colour the theme measures rather than faded by opacity: a
|
|
736
|
+
value nobody can read is a field nobody can check. */
|
|
737
|
+
color: var(--slotted-select-disabled-color, var(--slotted-disabled-foreground));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
[data-ui='select'][data-part='root'][data-disabled] > [data-part='trigger'],
|
|
741
|
+
[data-ui='select'][data-part='root'] > [data-part='trigger']:disabled {
|
|
742
|
+
cursor: not-allowed;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
[data-ui='select'] > [data-part='clear']:hover:not(:disabled):not([data-disabled]) {
|
|
746
|
+
background-color: var(
|
|
747
|
+
--slotted-select-action-hover-background,
|
|
748
|
+
var(--slotted-tone-secondary-subtle-hover)
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
[data-ui='select'] > [data-part='clear']:active:not(:disabled):not([data-disabled]) {
|
|
753
|
+
background-color: var(
|
|
754
|
+
--slotted-select-action-active-background,
|
|
755
|
+
var(--slotted-tone-secondary-subtle-active)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* The clear button carries its own ring rather than borrowing the frame's.
|
|
761
|
+
* Tabbing from the trigger to it has to move something visible, and a ring
|
|
762
|
+
* around the whole frame would sit still while focus moved inside it.
|
|
763
|
+
*/
|
|
764
|
+
[data-ui='select'] > [data-part='clear']:focus-visible {
|
|
765
|
+
outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
|
|
766
|
+
outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* ── The layer ────────────────────────────────────────────────────────── */
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* What `packages/core/src/positioner` writes `position`, `left` and `top`
|
|
773
|
+
* onto, and what slides and fades — the module leaves `transform` alone on
|
|
774
|
+
* purpose. The painted surface is one element in, as `tooltip`, `popover` and
|
|
775
|
+
* `menu` are built.
|
|
776
|
+
*/
|
|
777
|
+
[data-ui='select'][data-part='positioner'] {
|
|
778
|
+
--_surface: var(--slotted-select-surface, var(--slotted-surface-raised, #ffffff));
|
|
779
|
+
--_text: var(--slotted-select-content-text, var(--slotted-text, #0f172a));
|
|
780
|
+
--_muted: var(--slotted-select-content-text-muted, var(--slotted-text-muted, #5b6b7f));
|
|
781
|
+
--_border-color: var(
|
|
782
|
+
--slotted-select-content-border-color,
|
|
783
|
+
var(--slotted-border-subtle, #e2e8f0)
|
|
784
|
+
);
|
|
785
|
+
--_border-width: var(--slotted-border-width-thin, 1px);
|
|
786
|
+
--_radius: var(--slotted-select-content-radius, var(--slotted-radius-lg, 8px));
|
|
787
|
+
--_shadow: var(--slotted-select-content-shadow, var(--slotted-shadow-md));
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* The highlight is the *stronger* step of the neutral tone, not the one
|
|
791
|
+
* every other subtle surface in this library takes, and the reason is in
|
|
792
|
+
* the dark scheme's numbers: `--slotted-tone-secondary-subtle-hover` is
|
|
793
|
+
* `#18181b` where the raised popup is `#1c1c20`, so the highlight resolved
|
|
794
|
+
* four units **darker** than the ground it sits on and read as a hole
|
|
795
|
+
* rather than as a lift. The first dark capture of this family showed a
|
|
796
|
+
* highlighted option that could not be found. The `-active` step is
|
|
797
|
+
* `#27272a`, eleven units the other way, and it is the only member of the
|
|
798
|
+
* pair that lifts in all three schemes.
|
|
799
|
+
*/
|
|
800
|
+
--_highlight: var(--slotted-select-highlight, var(--slotted-tone-secondary-subtle-active));
|
|
801
|
+
--_highlight-active: var(--slotted-select-highlight-active, var(--slotted-tone-accent-surface));
|
|
802
|
+
--_selected-color: var(--slotted-select-selected-color, var(--slotted-tone-accent-text));
|
|
803
|
+
|
|
804
|
+
--_inset-block: var(--slotted-select-content-padding-block, var(--slotted-space-2, 0.25rem));
|
|
805
|
+
--_inset-inline: var(--slotted-select-content-padding-inline, var(--slotted-space-2, 0.25rem));
|
|
806
|
+
--_item-inset: var(--slotted-control-padding-inline-md, 0.8125rem);
|
|
807
|
+
--_item-height: var(--slotted-control-height-md, 34px);
|
|
808
|
+
--_item-radius: var(--slotted-select-item-radius, var(--slotted-control-radius));
|
|
809
|
+
--_content-font-size: var(--slotted-control-font-size-md, 13px);
|
|
810
|
+
--_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
|
|
811
|
+
--_content-icon-size: var(--slotted-control-icon-size-md, 16px);
|
|
812
|
+
--_column-gap: var(--slotted-control-gap-md, 0.5rem);
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The cap for `width="content"`. Measure belongs to `container`, so the
|
|
816
|
+
* family offers no axis for it and still has to stop a `max-content`
|
|
817
|
+
* surface from setting one option the width of the page.
|
|
818
|
+
*/
|
|
819
|
+
--_measure: var(--slotted-select-measure, 24rem);
|
|
820
|
+
|
|
821
|
+
/* Only the block inset: the positioner writes `left` and never `right`, and
|
|
822
|
+
`inset-inline-start` would resolve to `right` in a right-to-left document
|
|
823
|
+
and win over the measured value. */
|
|
824
|
+
position: absolute;
|
|
825
|
+
inset-block-start: 0;
|
|
826
|
+
|
|
827
|
+
z-index: calc(var(--slotted-z-index-popover, 1100) + var(--slotted-layer-depth, 0));
|
|
828
|
+
isolation: isolate;
|
|
829
|
+
|
|
830
|
+
box-sizing: border-box;
|
|
831
|
+
|
|
832
|
+
/* The layer is only a frame around the surface; the surface takes the
|
|
833
|
+
pointer, and the frame must not swallow a press meant for the page. */
|
|
834
|
+
pointer-events: none;
|
|
835
|
+
|
|
836
|
+
opacity: 0;
|
|
837
|
+
/**
|
|
838
|
+
* R7: the layer arrives along the axis it opens on — from one space step
|
|
839
|
+
* above its place when it opens downward, from one step below when it
|
|
840
|
+
* opens upward — and never along the inline axis. A `scale` from the
|
|
841
|
+
* corner the positioner named as origin moved the far edge by four per
|
|
842
|
+
* cent of the width, which read as an entrance from the left.
|
|
843
|
+
*/
|
|
844
|
+
--_enter-distance: var(--slotted-space-2, 0.25rem);
|
|
845
|
+
translate: 0 calc(-1 * var(--_enter-distance));
|
|
846
|
+
transition:
|
|
847
|
+
opacity var(--slotted-duration-short) var(--slotted-easing-exit),
|
|
848
|
+
translate var(--slotted-duration-short) var(--slotted-easing-exit);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
[data-ui='select'][data-part='positioner'][data-side='top'] {
|
|
852
|
+
translate: 0 var(--_enter-distance);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* A lateral side has no block direction to arrive from, so it only fades. */
|
|
856
|
+
[data-ui='select'][data-part='positioner']:is([data-side='left'], [data-side='right']) {
|
|
857
|
+
translate: 0 0;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* The entrance is a keyframe animation and not a transition. A transition
|
|
862
|
+
* starts from the style the browser computed last, and the positioner
|
|
863
|
+
* writes `data-side` in the same style update as the family opens the
|
|
864
|
+
* layer, so a transition set out from the closed style of the side the
|
|
865
|
+
* layer had before it had one — the bottom's — whatever side it opens on.
|
|
866
|
+
* A keyframe states where it starts. The exit stays a transition: it
|
|
867
|
+
* leaves from wherever the layer is, on the closed rule's durations.
|
|
868
|
+
*/
|
|
869
|
+
[data-ui='select'][data-part='positioner'][data-open] {
|
|
870
|
+
opacity: 1;
|
|
871
|
+
translate: 0 0;
|
|
872
|
+
transition: none;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Only a layer the positioner placed arrives: it writes `data-positioned`
|
|
877
|
+
* with the side, and a layer written open in static markup — the harness,
|
|
878
|
+
* a docs snippet — is simply there, and is photographed where it stands.
|
|
879
|
+
*/
|
|
880
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned] {
|
|
881
|
+
animation: slotted-select-enter-down var(--slotted-duration-medium) var(--slotted-easing-enter);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='top'] {
|
|
885
|
+
animation-name: slotted-select-enter-up;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned]:is(
|
|
889
|
+
[data-side='left'],
|
|
890
|
+
[data-side='right']
|
|
891
|
+
) {
|
|
892
|
+
animation-name: slotted-select-fade;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@keyframes slotted-select-enter-down {
|
|
896
|
+
from {
|
|
897
|
+
opacity: 0;
|
|
898
|
+
translate: 0 calc(-1 * var(--_enter-distance));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
@keyframes slotted-select-enter-up {
|
|
903
|
+
from {
|
|
904
|
+
opacity: 0;
|
|
905
|
+
translate: 0 var(--_enter-distance);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
@keyframes slotted-select-fade {
|
|
910
|
+
from {
|
|
911
|
+
opacity: 0;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* Stillness keeps the fade and drops the movement (R7). */
|
|
916
|
+
@media (prefers-reduced-motion: reduce) {
|
|
917
|
+
[data-ui='select'][data-part='positioner'],
|
|
918
|
+
[data-ui='select'][data-part='positioner'][data-side='top'],
|
|
919
|
+
[data-ui='select'][data-part='positioner'][data-side='left'],
|
|
920
|
+
[data-ui='select'][data-part='positioner'][data-side='right'] {
|
|
921
|
+
translate: 0 0;
|
|
922
|
+
transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned],
|
|
926
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='top'],
|
|
927
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='left'],
|
|
928
|
+
[data-ui='select'][data-part='positioner'][data-open][data-positioned][data-side='right'] {
|
|
929
|
+
animation-name: slotted-select-fade;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* The anchor scrolled out of its own clipper. The popup is still open and
|
|
934
|
+
its options still belong to a control the reader can no longer see. */
|
|
935
|
+
[data-ui='select'][data-part='positioner'][data-detached] {
|
|
936
|
+
visibility: hidden;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* `content` is the default: never narrower than the control, and wider
|
|
941
|
+
* when an option needs it, up to the measure and the available width, so a
|
|
942
|
+
* narrow control does not wrap its option labels. `anchor` pins the popup to
|
|
943
|
+
* the control's width exactly.
|
|
944
|
+
*/
|
|
945
|
+
[data-ui='select'][data-part='positioner'][data-width='anchor'] {
|
|
946
|
+
inline-size: var(--slotted-positioner-anchor-width, auto);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
[data-ui='select'][data-part='positioner'][data-width='content'] {
|
|
950
|
+
inline-size: max-content;
|
|
951
|
+
max-inline-size: min(var(--_measure), var(--slotted-positioner-available-width, 100vw));
|
|
952
|
+
/* Never narrower than the control it belongs to: a popup that shrank under
|
|
953
|
+
its own anchor reads as a second, smaller control. */
|
|
954
|
+
min-inline-size: var(--slotted-positioner-anchor-width, auto);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
[data-ui='select'][data-part='positioner'][data-size='sm'] {
|
|
958
|
+
--_item-inset: var(--slotted-control-padding-inline-sm, 0.625rem);
|
|
959
|
+
--_item-height: var(--slotted-control-height-sm, 30px);
|
|
960
|
+
--_content-font-size: var(--slotted-control-font-size-sm, 12px);
|
|
961
|
+
--_secondary-font-size: var(--slotted-font-size-xs, 0.75rem);
|
|
962
|
+
--_content-icon-size: var(--slotted-control-icon-size-sm, 14px);
|
|
963
|
+
--_column-gap: var(--slotted-control-gap-sm, 0.375rem);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
[data-ui='select'][data-part='positioner'][data-size='lg'] {
|
|
967
|
+
--_item-inset: var(--slotted-control-padding-inline-lg, 1rem);
|
|
968
|
+
--_item-height: var(--slotted-control-height-lg, 38px);
|
|
969
|
+
--_content-font-size: var(--slotted-control-font-size-lg, 14px);
|
|
970
|
+
--_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
|
|
971
|
+
--_content-icon-size: var(--slotted-control-icon-size-lg, 18px);
|
|
972
|
+
--_column-gap: var(--slotted-control-gap-lg, 0.5rem);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/* ── The surface ──────────────────────────────────────────────────────── */
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Three columns, and the height the positioner says is left. Radix, Base UI
|
|
979
|
+
* and React Aria each publish the remaining height and none of them bounds
|
|
980
|
+
* itself by default, so a list of a hundred options runs off the bottom of
|
|
981
|
+
* the screen. `overscroll-behavior` keeps the page still once the surface has
|
|
982
|
+
* scrolled to its end.
|
|
983
|
+
*/
|
|
984
|
+
[data-ui='select'] > [data-part='content'] {
|
|
985
|
+
background: var(--_surface);
|
|
986
|
+
border: var(--_border-width) solid var(--_border-color);
|
|
987
|
+
border-radius: var(--_radius);
|
|
988
|
+
box-shadow: var(--_shadow);
|
|
989
|
+
box-sizing: border-box;
|
|
990
|
+
color: var(--_text);
|
|
991
|
+
font-family: var(--slotted-font-family-sans);
|
|
992
|
+
font-size: var(--_content-font-size);
|
|
993
|
+
max-block-size: calc(
|
|
994
|
+
var(--slotted-positioner-available-height, 100vh) - var(--slotted-space-5, 0.75rem)
|
|
995
|
+
);
|
|
996
|
+
outline: none;
|
|
997
|
+
pointer-events: auto;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The three columns and the scroll, on whichever element holds the options.
|
|
1002
|
+
*
|
|
1003
|
+
* Without a search field that element is the surface itself, which is where
|
|
1004
|
+
* it has always been. With one the surface becomes a column of two — the
|
|
1005
|
+
* field, then the list — and the list is what scrolls: a field that scrolled
|
|
1006
|
+
* away with its own results is a field a reader loses on the fourth row, and
|
|
1007
|
+
* every implementation surveyed on 2026-09-08 that puts the field in the
|
|
1008
|
+
* popup keeps it still.
|
|
1009
|
+
*
|
|
1010
|
+
* `overscroll-behavior` keeps the page still once the scroller has reached
|
|
1011
|
+
* its end.
|
|
1012
|
+
*/
|
|
1013
|
+
[data-ui='select'] > [data-part='content']:not([data-search]),
|
|
1014
|
+
[data-ui='select'] [data-part='list'] {
|
|
1015
|
+
display: grid;
|
|
1016
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1017
|
+
overflow-y: auto;
|
|
1018
|
+
overscroll-behavior: contain;
|
|
1019
|
+
padding-block: var(--_inset-block);
|
|
1020
|
+
padding-inline: var(--_inset-inline);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
[data-ui='select'] > [data-part='content'][data-search] {
|
|
1024
|
+
display: flex;
|
|
1025
|
+
flex-direction: column;
|
|
1026
|
+
/* The field draws to the surface's own edges and the list keeps the inset,
|
|
1027
|
+
so each of the two states its own padding rather than sharing one. */
|
|
1028
|
+
padding: 0;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* `auto` as the flex basis measures the whole list and refuses to shrink;
|
|
1032
|
+
the surface is what holds the height, and the list is what gives. */
|
|
1033
|
+
[data-ui='select'] [data-part='list'] {
|
|
1034
|
+
flex: 1 1 auto;
|
|
1035
|
+
min-block-size: 0;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/* ── The search field ─────────────────────────────────────────────────── */
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* The row the query is typed into, at the top of the popup and only under
|
|
1042
|
+
* `search`.
|
|
1043
|
+
*
|
|
1044
|
+
* It is in the popup rather than in the control, and that is the whole of
|
|
1045
|
+
* what keeps this family apart from `combobox`: a combobox's typed text is a
|
|
1046
|
+
* value it commits, and this query is transient — it filters, it is emptied
|
|
1047
|
+
* when an option is taken, and the control goes on showing the chosen label
|
|
1048
|
+
* the entire time. `docs/component-features.md` §6 says the control takes no
|
|
1049
|
+
* typed text, and it still does not.
|
|
1050
|
+
*
|
|
1051
|
+
* A row the height of an option, so the field and the first result keep one
|
|
1052
|
+
* rhythm, with the rule under it drawn from the border scale rather than
|
|
1053
|
+
* from a shadow — the same rule the `chosen` band draws above the list.
|
|
1054
|
+
*/
|
|
1055
|
+
[data-ui='select'] [data-part='search'] {
|
|
1056
|
+
align-items: center;
|
|
1057
|
+
border-block-end: var(--_border-width) solid var(--_border-color);
|
|
1058
|
+
box-sizing: border-box;
|
|
1059
|
+
display: flex;
|
|
1060
|
+
flex: 0 0 auto;
|
|
1061
|
+
gap: var(--_column-gap);
|
|
1062
|
+
min-block-size: var(--_item-height);
|
|
1063
|
+
padding-block: var(--slotted-space-2, 0.25rem);
|
|
1064
|
+
/* The list is inset from the surface and each option is inset again inside
|
|
1065
|
+
it, so a row that took only the option's inset put its glyph four pixels
|
|
1066
|
+
to the start of every label under it. The two insets are added here, and
|
|
1067
|
+
the magnifier opens the same column the labels do. */
|
|
1068
|
+
padding-inline: calc(var(--_inset-inline) + var(--_item-inset));
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* The magnifier. It is what makes the row a field rather than a heading: a
|
|
1073
|
+
* bare line of placeholder text at the top of a panel reads as a title, and
|
|
1074
|
+
* a reader does not type into a title.
|
|
1075
|
+
*/
|
|
1076
|
+
[data-ui='select'] [data-part='search-icon'] {
|
|
1077
|
+
--slotted-icon-size: var(--_content-icon-size);
|
|
1078
|
+
|
|
1079
|
+
align-items: center;
|
|
1080
|
+
block-size: var(--_content-icon-size);
|
|
1081
|
+
color: var(--slotted-select-search-icon-color, var(--_muted));
|
|
1082
|
+
display: flex;
|
|
1083
|
+
flex: 0 0 auto;
|
|
1084
|
+
inline-size: var(--_content-icon-size);
|
|
1085
|
+
justify-content: center;
|
|
1086
|
+
pointer-events: none;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* The field itself, which gives up every frame of its own: the surface is
|
|
1091
|
+
* already a bordered panel, and a second border a step inside it would draw
|
|
1092
|
+
* a box inside a box for one line of text.
|
|
1093
|
+
*
|
|
1094
|
+
* **No focus ring.** The field takes focus as the popup opens and holds it
|
|
1095
|
+
* until the popup closes — there is no state in which this layer is open and
|
|
1096
|
+
* this element is not focused — so a ring here would be a permanent border
|
|
1097
|
+
* rather than a state, and the one thing on the surface that never changes
|
|
1098
|
+
* would be the loudest. What says where focus is, is the caret.
|
|
1099
|
+
*/
|
|
1100
|
+
[data-ui='select'] [data-part='search-input'] {
|
|
1101
|
+
background-color: transparent;
|
|
1102
|
+
border: 0;
|
|
1103
|
+
color: inherit;
|
|
1104
|
+
flex: 1 1 auto;
|
|
1105
|
+
font: inherit;
|
|
1106
|
+
/* The intrinsic width of an `input` is about twenty characters, and a flex
|
|
1107
|
+
item refuses to shrink below it without this. */
|
|
1108
|
+
min-inline-size: 0;
|
|
1109
|
+
outline: none;
|
|
1110
|
+
padding: 0;
|
|
1111
|
+
text-overflow: ellipsis;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
[data-ui='select'] [data-part='search-input']::placeholder {
|
|
1115
|
+
color: var(--slotted-select-placeholder-color, var(--_muted));
|
|
1116
|
+
text-overflow: ellipsis;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Everything that is chosen, at the top of the popup (SL10).
|
|
1121
|
+
*
|
|
1122
|
+
* The control holds one line, so twelve chosen values are three chips and a
|
|
1123
|
+
* count; here they are all of them. The band wraps and takes about two rows
|
|
1124
|
+
* of its own before it scrolls, and it scrolls **itself** — the list under it
|
|
1125
|
+
* keeps its own scroll, and `overscroll-behavior` stops one from carrying the
|
|
1126
|
+
* other once it reaches its end.
|
|
1127
|
+
*
|
|
1128
|
+
* It is stuck to the top of the scrollport rather than laid above it: the
|
|
1129
|
+
* surface is the scroller as well as the box, so a band that merely came
|
|
1130
|
+
* first would scroll away with the options. The negative insets take it out
|
|
1131
|
+
* to the surface's own edges, which is where the rule between it and the list
|
|
1132
|
+
* has to end — a rule from the border scale, not a shadow.
|
|
1133
|
+
*/
|
|
1134
|
+
[data-ui='select'] [data-part='chosen'] {
|
|
1135
|
+
background: var(--_surface);
|
|
1136
|
+
border-block-end: var(--_border-width) solid var(--_border-color);
|
|
1137
|
+
box-sizing: border-box;
|
|
1138
|
+
display: flex;
|
|
1139
|
+
flex-wrap: wrap;
|
|
1140
|
+
gap: var(--slotted-space-2, 0.25rem);
|
|
1141
|
+
grid-column: 1 / -1;
|
|
1142
|
+
/* Its own padding stands where the surface's would, so nothing shows above
|
|
1143
|
+
it as the list scrolls under. */
|
|
1144
|
+
inset-block-start: calc(-1 * var(--_inset-block));
|
|
1145
|
+
margin-block: calc(-1 * var(--_inset-block)) var(--_inset-block);
|
|
1146
|
+
margin-inline: calc(-1 * var(--_inset-inline));
|
|
1147
|
+
/**
|
|
1148
|
+
* Two rows of chips, the gap between them, and the air above the first.
|
|
1149
|
+
* Two *whole* rows: a chip drawn to half its height is not an affordance,
|
|
1150
|
+
* it is a chip that looks broken. What says there is more is the scrollbar
|
|
1151
|
+
* and the rule under it. The row is the target minimum, which is the floor
|
|
1152
|
+
* a chip's own remove control puts under it everywhere else (TG2/SL8), so
|
|
1153
|
+
* a chip at the small step never overruns the cap.
|
|
1154
|
+
*/
|
|
1155
|
+
max-block-size: calc(
|
|
1156
|
+
2 * var(--slotted-target-min, 24px) + var(--slotted-space-2, 0.25rem) + var(--_inset-block)
|
|
1157
|
+
);
|
|
1158
|
+
min-inline-size: 0;
|
|
1159
|
+
overflow-y: auto;
|
|
1160
|
+
overscroll-behavior: contain;
|
|
1161
|
+
padding-block: var(--_inset-block);
|
|
1162
|
+
padding-inline: var(--_inset-inline);
|
|
1163
|
+
position: sticky;
|
|
1164
|
+
z-index: 1;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* The surface never takes a ring. Focus stays on the control for the whole
|
|
1169
|
+
* interaction — the highlight is `aria-activedescendant`, not focus — so a
|
|
1170
|
+
* ring here would draw a box around something nobody is operating.
|
|
1171
|
+
*/
|
|
1172
|
+
[data-ui='select'] > [data-part='content']:focus-visible {
|
|
1173
|
+
outline: none;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/* ── Groups ───────────────────────────────────────────────────────────── */
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* A group spans every column and inherits them, so its options keep the
|
|
1180
|
+
* surface's columns instead of starting a second set. Without the subgrid a
|
|
1181
|
+
* group would align its own labels and disagree with the options outside it.
|
|
1182
|
+
*/
|
|
1183
|
+
[data-ui='select'] [data-part='group'] {
|
|
1184
|
+
display: grid;
|
|
1185
|
+
grid-column: 1 / -1;
|
|
1186
|
+
grid-template-columns: subgrid;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* An option the query rejected, and a group whose options all went. Both
|
|
1191
|
+
* carry `hidden` — Angular hides each rejected option rather than leaving the
|
|
1192
|
+
* list, and both bindings hide an emptied group — and `display: grid` on
|
|
1193
|
+
* either beats the user agent's own `[hidden] { display: none }`, because an
|
|
1194
|
+
* author rule wins over the user agent sheet. Without this the search
|
|
1195
|
+
* filtered nothing the reader could see.
|
|
1196
|
+
*/
|
|
1197
|
+
[data-ui='select'] [data-part='item'][hidden],
|
|
1198
|
+
[data-ui='select'] [data-part='group'][hidden] {
|
|
1199
|
+
display: none;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
[data-ui='select'] [data-part='group-label'] {
|
|
1203
|
+
color: var(--_muted);
|
|
1204
|
+
font-size: var(--_secondary-font-size);
|
|
1205
|
+
font-weight: var(--slotted-font-weight-semibold, 600);
|
|
1206
|
+
grid-column: 1 / -1;
|
|
1207
|
+
line-height: var(--slotted-line-height-sm, 1.45);
|
|
1208
|
+
padding-block: var(--slotted-space-2, 0.25rem);
|
|
1209
|
+
padding-inline: var(--_item-inset);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/* ── Options ──────────────────────────────────────────────────────────── */
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* `role="option"` on a `div`. Unlike `menu`'s item there is no native
|
|
1216
|
+
* element to keep: the listbox pattern's option is not a button, it is not
|
|
1217
|
+
* separately focusable — DOM focus stays on the control — and a `button`
|
|
1218
|
+
* here would put a widget inside a widget and add a tab stop the pattern
|
|
1219
|
+
* excludes by name.
|
|
1220
|
+
*
|
|
1221
|
+
* `min-block-size` rather than a height, because an option with a
|
|
1222
|
+
* description is two lines tall and still has to clear the target minimum.
|
|
1223
|
+
*/
|
|
1224
|
+
[data-ui='select'] [data-part='item'] {
|
|
1225
|
+
align-content: center;
|
|
1226
|
+
border-radius: var(--_item-radius);
|
|
1227
|
+
box-sizing: border-box;
|
|
1228
|
+
/**
|
|
1229
|
+
* The hand (SL11). Nine of the twelve implementations surveyed draw it;
|
|
1230
|
+
* the three that draw an arrow are the three that follow the native
|
|
1231
|
+
* `select`, whose popup the operating system paints. A row that commits a
|
|
1232
|
+
* value and closes the surface is a press. `menu` kept the arrow for a
|
|
1233
|
+
* while on the argument that a menu item follows the platform's own
|
|
1234
|
+
* menus, and dropped it: its surface is a `div` on the page as much as
|
|
1235
|
+
* this one is.
|
|
1236
|
+
*/
|
|
1237
|
+
cursor: pointer;
|
|
1238
|
+
display: grid;
|
|
1239
|
+
grid-column: 1 / -1;
|
|
1240
|
+
grid-template-columns: subgrid;
|
|
1241
|
+
/**
|
|
1242
|
+
* The line box belongs to the option and every cell inherits it. `1lh` on a
|
|
1243
|
+
* mark resolves against the mark's *own* line-height, so a mark inside a
|
|
1244
|
+
* row that left the line box at `normal` measures fifteen pixels while the
|
|
1245
|
+
* label beside it measures nineteen and a half — and start-aligned in the
|
|
1246
|
+
* same row, the mark sits two pixels above the text it annotates.
|
|
1247
|
+
*/
|
|
1248
|
+
line-height: var(--slotted-line-height-md, 1.5);
|
|
1249
|
+
min-block-size: var(--_item-height);
|
|
1250
|
+
padding-block: var(--slotted-space-2, 0.25rem);
|
|
1251
|
+
padding-inline: var(--_item-inset);
|
|
1252
|
+
row-gap: var(--slotted-space-1, 0.125rem);
|
|
1253
|
+
/* A press that lands on an option must not also select the label's text,
|
|
1254
|
+
which is what a double-click on a list otherwise does. */
|
|
1255
|
+
user-select: none;
|
|
1256
|
+
transition:
|
|
1257
|
+
background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
|
|
1258
|
+
var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
|
|
1259
|
+
color var(--slotted-control-transition-duration, var(--slotted-duration-short))
|
|
1260
|
+
var(--slotted-control-transition-easing, var(--slotted-easing-standard));
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* The highlight. It is `data-current` rather than a hover or focus selector,
|
|
1265
|
+
* because the highlight is where the reader is and the pointer and the
|
|
1266
|
+
* keyboard both move it: entering an option with the pointer sets
|
|
1267
|
+
* `aria-activedescendant` to it, so exactly one option is highlighted
|
|
1268
|
+
* whichever device is driving, and the state a screen reader announces and
|
|
1269
|
+
* the state the eye sees are the same state.
|
|
1270
|
+
*/
|
|
1271
|
+
[data-ui='select'] [data-part='item'][data-current] {
|
|
1272
|
+
background-color: var(--_highlight);
|
|
1273
|
+
/**
|
|
1274
|
+
* A ring as well as a ground, because the ground alone carries the whole
|
|
1275
|
+
* signal and eleven units of lift is not enough to carry it. The APG's own
|
|
1276
|
+
* select-only example marks its current option with an outline for the
|
|
1277
|
+
* same reason: nothing on this surface holds focus — DOM focus is on the
|
|
1278
|
+
* control — so there is no focus ring anywhere to say where the reader is.
|
|
1279
|
+
*
|
|
1280
|
+
* An inset shadow rather than an outline, so it follows the option's own
|
|
1281
|
+
* radius and never overflows the surface's rounded edge, and one border
|
|
1282
|
+
* width rather than two, because it is a cursor and not a focus indicator.
|
|
1283
|
+
*/
|
|
1284
|
+
box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
|
|
1285
|
+
var(--slotted-select-highlight-ring, var(--slotted-tone-accent-border));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
[data-ui='select'] [data-part='item']:not([data-disabled]):active {
|
|
1289
|
+
background-color: var(--_highlight-active);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Chosen, which is not the same as highlighted. The check in the last column
|
|
1294
|
+
* is the primary signal — it survives forced colours, where every tone ground
|
|
1295
|
+
* becomes `Canvas` — and the accent ink is the secondary one, so the row a
|
|
1296
|
+
* reader is looking for is findable at a glance in a list of forty.
|
|
1297
|
+
*/
|
|
1298
|
+
[data-ui='select'] [data-part='item'][data-selected] {
|
|
1299
|
+
color: var(--_selected-color);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Two chosen neighbours are one block (LB2). Every item keeps its own
|
|
1304
|
+
* radius, so two selected siblings painted with the same ground met at a
|
|
1305
|
+
* waist — four rounded corners inside the block — and a multiple selection
|
|
1306
|
+
* read as a column of separate chips. The second gives up its top corners
|
|
1307
|
+
* and the first its bottom ones, so the block is rounded only where the
|
|
1308
|
+
* selection begins and ends. `select` and `combobox` draw the same ground
|
|
1309
|
+
* and answer the same way; `selection-block.test.mjs` holds the three to it.
|
|
1310
|
+
*/
|
|
1311
|
+
[data-ui='select'] [data-part='item'][data-selected] + [data-part='item'][data-selected] {
|
|
1312
|
+
border-start-end-radius: 0;
|
|
1313
|
+
border-start-start-radius: 0;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
[data-ui='select'] [data-part='item'][data-selected]:has(+ [data-part='item'][data-selected]) {
|
|
1317
|
+
border-end-end-radius: 0;
|
|
1318
|
+
border-end-start-radius: 0;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
[data-ui='select'] [data-part='item'][data-selected] > [data-part='label'] {
|
|
1322
|
+
font-weight: var(--slotted-font-weight-medium, 500);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* A disabled option is announced, keeps its place and cannot be chosen. It
|
|
1327
|
+
* therefore has to answer the highlight with something, and answer it more
|
|
1328
|
+
* quietly than an option that would do anything: the ground is the theme's
|
|
1329
|
+
* disabled surface rather than the highlight, and the text stays legible
|
|
1330
|
+
* instead of being faded toward it.
|
|
1331
|
+
*/
|
|
1332
|
+
[data-ui='select'] [data-part='item'][data-disabled] {
|
|
1333
|
+
color: var(--slotted-disabled-foreground, #64748b);
|
|
1334
|
+
cursor: not-allowed;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* The highlight can land on an option nobody can choose, because a disabled
|
|
1339
|
+
* option keeps its place in the collection and the pattern requires it to be
|
|
1340
|
+
* announced. It must not wear the accent cursor while it is there: an accent
|
|
1341
|
+
* ring on a row that answers nothing says "act here" to the one reader who
|
|
1342
|
+
* cannot. The ground and the ring both step down to the theme's disabled
|
|
1343
|
+
* pair, and the row stays legible rather than being faded toward it.
|
|
1344
|
+
*/
|
|
1345
|
+
/**
|
|
1346
|
+
* The highlight can land on an option nobody can choose, because a disabled
|
|
1347
|
+
* option keeps its place in the collection and the pattern requires it to be
|
|
1348
|
+
* announced.
|
|
1349
|
+
*
|
|
1350
|
+
* **The ground stays the highlight's.** Painting the theme's disabled surface
|
|
1351
|
+
* here was the first answer and it was wrong in the dark scheme, for the same
|
|
1352
|
+
* reason the highlight itself was: `--slotted-disabled-background` is
|
|
1353
|
+
* `#18181b` where the raised popup is `#1c1c20`, so the cursor *sank* on a
|
|
1354
|
+
* disabled row and rose on every other one — a reader arrowing down the list
|
|
1355
|
+
* would watch it disappear. A cursor lifts, always. What the row cannot do is
|
|
1356
|
+
* said by the ink and by the ring, which both step down to the disabled pair.
|
|
1357
|
+
*/
|
|
1358
|
+
[data-ui='select'] [data-part='item'][data-disabled][data-current] {
|
|
1359
|
+
box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
|
|
1360
|
+
var(--slotted-disabled-border, #cbd5e1);
|
|
1361
|
+
color: var(--slotted-disabled-foreground, #64748b);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/* ── The cells ────────────────────────────────────────────────────────── */
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* One line box tall, so a mark beside a label that wrapped sits on the first
|
|
1368
|
+
* line rather than in the middle of the option — and, because the option
|
|
1369
|
+
* states the line box every cell inherits, exactly as tall as the line it
|
|
1370
|
+
* sits beside.
|
|
1371
|
+
*/
|
|
1372
|
+
[data-ui='select'] :is([data-part='indicator'], [data-part='icon']) {
|
|
1373
|
+
block-size: 1lh;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Column one, and the reason an option with artwork and one without line
|
|
1378
|
+
* their labels up: the column is as wide as the content icon step whether or
|
|
1379
|
+
* not a given option fills it.
|
|
1380
|
+
*
|
|
1381
|
+
* The box holds content and paints none: a `Glyph` in an `Icon` box, or
|
|
1382
|
+
* artwork of the consumer's own, centred and sized from the column through
|
|
1383
|
+
* `--slotted-icon-size`. No stylesheet fills it.
|
|
1384
|
+
*/
|
|
1385
|
+
[data-ui='select'] [data-part='icon'] {
|
|
1386
|
+
--slotted-icon-size: var(--_content-icon-size);
|
|
1387
|
+
|
|
1388
|
+
align-items: center;
|
|
1389
|
+
display: flex;
|
|
1390
|
+
grid-column: 1;
|
|
1391
|
+
grid-row: 1;
|
|
1392
|
+
inline-size: var(--_content-icon-size);
|
|
1393
|
+
justify-content: center;
|
|
1394
|
+
justify-self: center;
|
|
1395
|
+
margin-inline-end: var(--_column-gap);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/* Column two, and the only column that gives way. It is scoped to the option
|
|
1399
|
+
rather than to the family: a chip in the control carries a `label` of its
|
|
1400
|
+
own, and this rule would have put it in the popup's second column (SL4). */
|
|
1401
|
+
[data-ui='select'] [data-part='item'] > [data-part='label'] {
|
|
1402
|
+
grid-column: 2;
|
|
1403
|
+
grid-row: 1;
|
|
1404
|
+
min-inline-size: 0;
|
|
1405
|
+
/**
|
|
1406
|
+
* A label longer than the popup wraps to two lines and then truncates,
|
|
1407
|
+
* with the whole string still in the DOM. Radix, Base UI and Ark all let it
|
|
1408
|
+
* wrap to as many lines as it takes, so one translated option can be five
|
|
1409
|
+
* lines tall and the list stops being scannable; Material truncates at one
|
|
1410
|
+
* line and loses the text. Two lines is the compromise, and the second one
|
|
1411
|
+
* is where a name and its qualifier fit.
|
|
1412
|
+
*/
|
|
1413
|
+
display: -webkit-box;
|
|
1414
|
+
-webkit-box-orient: vertical;
|
|
1415
|
+
-webkit-line-clamp: 2;
|
|
1416
|
+
overflow: hidden;
|
|
1417
|
+
/* A sixty-character identifier with no spaces breaks rather than escaping
|
|
1418
|
+
the surface, which is what it does in two of the surveyed libraries. */
|
|
1419
|
+
overflow-wrap: anywhere;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
[data-ui='select'] [data-part='description'] {
|
|
1423
|
+
color: var(--_muted);
|
|
1424
|
+
font-size: var(--_secondary-font-size);
|
|
1425
|
+
grid-column: 2;
|
|
1426
|
+
grid-row: 2;
|
|
1427
|
+
line-height: var(--slotted-line-height-sm, 1.45);
|
|
1428
|
+
min-inline-size: 0;
|
|
1429
|
+
overflow-wrap: anywhere;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
[data-ui='select'] [data-part='item'][data-disabled] > [data-part='description'] {
|
|
1433
|
+
color: inherit;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Column three, and the mark that says an option was chosen. The box is
|
|
1438
|
+
* reserved whether or not the check is drawn, so a list does not shift by
|
|
1439
|
+
* sixteen pixels as the reader moves through it.
|
|
1440
|
+
*/
|
|
1441
|
+
[data-ui='select'] [data-part='indicator'] {
|
|
1442
|
+
--slotted-icon-size: var(--_content-icon-size);
|
|
1443
|
+
|
|
1444
|
+
align-items: center;
|
|
1445
|
+
display: flex;
|
|
1446
|
+
grid-column: 3;
|
|
1447
|
+
grid-row: 1;
|
|
1448
|
+
inline-size: var(--_content-icon-size);
|
|
1449
|
+
justify-content: center;
|
|
1450
|
+
justify-self: end;
|
|
1451
|
+
margin-inline-start: var(--_column-gap);
|
|
1452
|
+
opacity: 0;
|
|
1453
|
+
/* The check appears rather than blinking: the one movement inside the
|
|
1454
|
+
popup, and it is what confirms the press the reader just made. */
|
|
1455
|
+
transition: opacity var(--slotted-duration-short) var(--slotted-easing-standard);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
[data-ui='select'] [data-part='item'][data-selected] > [data-part='indicator'] {
|
|
1459
|
+
opacity: 1;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/* ── Separator, empty, status ─────────────────────────────────────────── */
|
|
1463
|
+
|
|
1464
|
+
[data-ui='select'] [data-part='separator'] {
|
|
1465
|
+
background-color: var(--slotted-select-separator-color, var(--_border-color));
|
|
1466
|
+
block-size: var(--_border-width);
|
|
1467
|
+
border: 0;
|
|
1468
|
+
grid-column: 1 / -1;
|
|
1469
|
+
margin-block: var(--slotted-space-2, 0.25rem);
|
|
1470
|
+
margin-inline: 0;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* A list with nothing in it, and a list still waiting. Both keep the
|
|
1475
|
+
* surface's measure rather than collapsing to a two-pixel box, which is what
|
|
1476
|
+
* a consumer sees flash when the state is not painted at all. React Aria
|
|
1477
|
+
* refuses to open on an empty collection and Radix paints nothing; the popup
|
|
1478
|
+
* here opens onto a sentence.
|
|
1479
|
+
*/
|
|
1480
|
+
[data-ui='select'] :is([data-part='empty'], [data-part='status']) {
|
|
1481
|
+
color: var(--_muted);
|
|
1482
|
+
/**
|
|
1483
|
+
* Its own bidi run, ordered by its own content. "3 results" is a European
|
|
1484
|
+
* number, a neutral space and a Latin word, so in a right-to-left document
|
|
1485
|
+
* the space resolves to the paragraph and the two runs swap: the first
|
|
1486
|
+
* right-to-left capture of this family printed "results 3". `plaintext`
|
|
1487
|
+
* takes the direction from the sentence's own first strong character, so an
|
|
1488
|
+
* English count reads as itself and a translated one reads as itself — the
|
|
1489
|
+
* same repair `text-field` makes for an adornment, and for the same reason.
|
|
1490
|
+
* These two parts are centred, so taking their own direction costs no
|
|
1491
|
+
* alignment; an option's label is the consumer's content and keeps the
|
|
1492
|
+
* document's direction unless the consumer says otherwise.
|
|
1493
|
+
*/
|
|
1494
|
+
unicode-bidi: plaintext;
|
|
1495
|
+
grid-column: 1 / -1;
|
|
1496
|
+
/**
|
|
1497
|
+
* Wide enough that a one-word message does not draw a popup narrower than a
|
|
1498
|
+
* word, and never wider than the popup it is in: at `width="anchor"` the
|
|
1499
|
+
* surface is exactly the control's width, and a bare `12rem` on a child
|
|
1500
|
+
* overflows the grid the moment the control is narrower than that.
|
|
1501
|
+
*/
|
|
1502
|
+
min-inline-size: min(12rem, 100%);
|
|
1503
|
+
padding-block: var(--slotted-space-5, 0.75rem);
|
|
1504
|
+
padding-inline: var(--_item-inset);
|
|
1505
|
+
text-align: center;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Under `search` the status row is also the live region that announces how
|
|
1510
|
+
* many options survived the query, so it is present on every open popup and
|
|
1511
|
+
* empty on most of them. A row with no text takes no space; a row with text
|
|
1512
|
+
* takes the same space as the empty message beside it. `combobox` draws the
|
|
1513
|
+
* count the same way, in the same place, for the same reason.
|
|
1514
|
+
*/
|
|
1515
|
+
[data-ui='select'] [data-part='status']:empty {
|
|
1516
|
+
display: none;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* The wait, as the shape of the list that is coming. Three rows the height of
|
|
1521
|
+
* an option rather than one sentence, so the surface a reader is looking at
|
|
1522
|
+
* while the answer travels is the surface they will go on looking at
|
|
1523
|
+
* (SL6/CB5). The bars are `skeleton/skeleton.css`'s — the family places them
|
|
1524
|
+
* and states nothing about how a placeholder is painted.
|
|
1525
|
+
*
|
|
1526
|
+
* The three widths descend, because three identical bars read as a table and
|
|
1527
|
+
* a list of labels is ragged. They are the consumer's nothing, so they are
|
|
1528
|
+
* hidden from assistive technology: the `status` row beside them is the live
|
|
1529
|
+
* region and it already says what is happening.
|
|
1530
|
+
*/
|
|
1531
|
+
[data-ui='select'] [data-part='placeholder'] {
|
|
1532
|
+
display: grid;
|
|
1533
|
+
/* The surface is a grid of option columns; the rows span all of them, as
|
|
1534
|
+
the status and empty rows do. */
|
|
1535
|
+
grid-column: 1 / -1;
|
|
1536
|
+
padding-block: var(--slotted-space-2, 0.25rem);
|
|
1537
|
+
padding-inline: var(--_item-inset);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
[data-ui='select'] [data-part='placeholder'] > [data-ui='skeleton'] {
|
|
1541
|
+
align-self: center;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Each row is an option: the same minimum height, so three of them measure
|
|
1546
|
+
* what three options measure and the popup does not resize when they land.
|
|
1547
|
+
*/
|
|
1548
|
+
[data-ui='select'] [data-part='placeholder'] > * {
|
|
1549
|
+
align-items: center;
|
|
1550
|
+
display: flex;
|
|
1551
|
+
min-block-size: var(--_item-height);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
[data-ui='select'] [data-part='placeholder'] > :nth-child(2) [data-ui='skeleton'] {
|
|
1555
|
+
inline-size: 78%;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
[data-ui='select'] [data-part='placeholder'] > :nth-child(3) [data-ui='skeleton'] {
|
|
1559
|
+
inline-size: 56%;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* The status row while an answer is travelling. It no longer draws a pulsing
|
|
1564
|
+
* dot of its own: the control carries the ring and the popup carries the
|
|
1565
|
+
* three bars, and a third repeating motion beside them was one answer too
|
|
1566
|
+
* many for one wait (SL6).
|
|
1567
|
+
*/
|
|
1568
|
+
|
|
1569
|
+
[data-ui='select'] > [data-part='content'][data-loading] {
|
|
1570
|
+
cursor: progress;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/* ── Forced colours ───────────────────────────────────────────────────── */
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* The one thing the theme cannot reach. Every tone surface becomes `Canvas`
|
|
1577
|
+
* under forced colours, so the highlight — which is this family's answer to
|
|
1578
|
+
* "which option am I on", not decoration — would disappear and leave a reader
|
|
1579
|
+
* with no way to tell.
|
|
1580
|
+
*/
|
|
1581
|
+
@media (forced-colors: active) {
|
|
1582
|
+
[data-ui='select'] [data-part='item'][data-current] {
|
|
1583
|
+
background-color: Highlight;
|
|
1584
|
+
color: HighlightText;
|
|
1585
|
+
forced-color-adjust: none;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
[data-ui='select'] [data-part='item'][data-disabled][data-current] {
|
|
1589
|
+
background-color: Highlight;
|
|
1590
|
+
color: GrayText;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|