@charcuterie/ui 4.22.0 → 4.22.1
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/dist/Picker/Picker.d.ts +15 -0
- package/dist/Picker/Picker.d.ts.map +1 -1
- package/dist/Picker/Picker.js +26 -4
- package/dist/Picker/Picker.js.map +1 -1
- package/package.json +1 -1
- package/src/Picker/Picker.mdx +14 -0
- package/src/Picker/Picker.stories.tsx +46 -0
- package/src/Picker/Picker.test.tsx +91 -2
- package/src/Picker/Picker.tsx +43 -4
package/dist/Picker/Picker.d.ts
CHANGED
|
@@ -84,6 +84,21 @@ export type PickerProps = Omit<ComponentPropsWithRef<"button">, "disabled" | "on
|
|
|
84
84
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
85
85
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
86
86
|
* `onClick`, `form`, …) passes straight through.
|
|
87
|
+
*
|
|
88
|
+
* ### A stretched trigger reads like a select, not a button
|
|
89
|
+
*
|
|
90
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
91
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
92
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
93
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
94
|
+
* at the inline start, in a span that grows into the spare width and
|
|
95
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
96
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
97
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
98
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
99
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
100
|
+
* at its content width has no spare width to distribute, so it renders
|
|
101
|
+
* exactly as it did before.
|
|
87
102
|
*/
|
|
88
103
|
export declare const Picker: ({ appearance, className, iconEnd, intent, isDisabled, label, onChange, options, placement, placeholder, size, value, ...buttonProps }: PickerProps) => ReactNode;
|
|
89
104
|
//# sourceMappingURL=Picker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAIzD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAA;AAwBtC,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,OAAO,CAClC,GAAG;IACF,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,eAAO,MAAM,MAAM,GAAI,uIAcpB,WAAW,KAAG,SA2EhB,CAAA"}
|
package/dist/Picker/Picker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useVisibility } from "@charcuterie/logic";
|
|
3
3
|
import { Button } from "../Button/Button.js";
|
|
4
4
|
import { Listbox } from "../Listbox/Listbox.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
5
6
|
/**
|
|
6
7
|
* A constant element rather than a component, for the same reason
|
|
7
8
|
* `Select` inlines its own: a second component in this file trips the
|
|
@@ -62,6 +63,21 @@ const CHEVRON_DOWN = (_jsx("svg", { "aria-hidden": "true", className: "size-4",
|
|
|
62
63
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
63
64
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
64
65
|
* `onClick`, `form`, …) passes straight through.
|
|
66
|
+
*
|
|
67
|
+
* ### A stretched trigger reads like a select, not a button
|
|
68
|
+
*
|
|
69
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
70
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
71
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
72
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
73
|
+
* at the inline start, in a span that grows into the spare width and
|
|
74
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
75
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
76
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
77
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
78
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
79
|
+
* at its content width has no spare width to distribute, so it renders
|
|
80
|
+
* exactly as it did before.
|
|
65
81
|
*/
|
|
66
82
|
export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DOWN, intent = "neutral", isDisabled = false, label, onChange, options, placement, placeholder, size, value, ...buttonProps }) => {
|
|
67
83
|
const { hide, isVisible, toggle } = useVisibility();
|
|
@@ -73,6 +89,14 @@ export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DO
|
|
|
73
89
|
? currentOption.label
|
|
74
90
|
: undefined));
|
|
75
91
|
const triggerText = currentText ?? placeholder ?? "";
|
|
92
|
+
const triggerContent = currentOption === undefined
|
|
93
|
+
? placeholder
|
|
94
|
+
: currentOption.label;
|
|
95
|
+
// No content, no span: an empty flex item would still earn a `gap`
|
|
96
|
+
// before the chevron and widen a content-width trigger.
|
|
97
|
+
const hasTriggerContent = triggerContent !== undefined &&
|
|
98
|
+
triggerContent !== null &&
|
|
99
|
+
triggerContent !== "";
|
|
76
100
|
return (_jsx(Listbox
|
|
77
101
|
// A disabled control must not open, and hiding the panel is a
|
|
78
102
|
// truer way to say that than trusting the trigger's click alone —
|
|
@@ -81,11 +105,9 @@ export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DO
|
|
|
81
105
|
// A disabled control must not open, and hiding the panel is a
|
|
82
106
|
// truer way to say that than trusting the trigger's click alone —
|
|
83
107
|
// Escape and outside-press both route through `onDismiss`.
|
|
84
|
-
isVisible: isVisible && !isDisabled, onDismiss: hide, onSelect: onChange, options: options, placement: placement, selectedValue: value, trigger: _jsx(Button, { ...buttonProps, appearance: appearance, "aria-label": `${label}: ${triggerText}`, className: className, iconEnd: iconEnd, intent: intent, isDisabled: isDisabled, onClick: (clickEvent) => {
|
|
108
|
+
isVisible: isVisible && !isDisabled, onDismiss: hide, onSelect: onChange, options: options, placement: placement, selectedValue: value, trigger: _jsx(Button, { ...buttonProps, appearance: appearance, "aria-label": `${label}: ${triggerText}`, className: toClassName("justify-between", className), iconEnd: iconEnd, intent: intent, isDisabled: isDisabled, onClick: (clickEvent) => {
|
|
85
109
|
buttonProps.onClick?.(clickEvent);
|
|
86
110
|
toggle();
|
|
87
|
-
}, size: size, type: "button", children:
|
|
88
|
-
? placeholder
|
|
89
|
-
: currentOption.label }) }));
|
|
111
|
+
}, size: size, type: "button", children: hasTriggerContent ? (_jsx("span", { className: "-mx-1 min-w-0 grow overflow-x-clip px-1 text-start text-ellipsis whitespace-nowrap", children: triggerContent })) : null }) }));
|
|
90
112
|
};
|
|
91
113
|
//# sourceMappingURL=Picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAwB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAI/C;;;;GAIG;AACH,MAAM,YAAY,GAAG,CACnB,6BACc,MAAM,EAClB,SAAS,EAAC,QAAQ,EAClB,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,IAAI,EACjB,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,cAAc,GAAG,GACrB,CACP,CAAA;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,UAAU,GAAG,SAAS,EACtB,SAAS,EACT,OAAO,GAAG,YAAY,EACtB,MAAM,GAAG,SAAS,EAClB,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAS,EACT,WAAW,EACX,IAAI,EACJ,KAAK,EACL,GAAG,WAAW,EACF,EAAa,EAAE;IAC3B,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAA;IAEnD,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAChC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CACnC,CAAA;IAED,MAAM,WAAW,GACf,aAAa,KAAK,SAAS;QACzB,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS;YACxB,CAAC,OAAO,aAAa,CAAC,KAAK,KAAK,QAAQ;gBACtC,CAAC,CAAC,aAAa,CAAC,KAAK;gBACrB,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IAErB,MAAM,WAAW,GAAG,WAAW,IAAI,WAAW,IAAI,EAAE,CAAA;IAEpD,MAAM,cAAc,GAClB,aAAa,KAAK,SAAS;QACzB,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,aAAa,CAAC,KAAK,CAAA;IAEzB,mEAAmE;IACnE,wDAAwD;IACxD,MAAM,iBAAiB,GACrB,cAAc,KAAK,SAAS;QAC5B,cAAc,KAAK,IAAI;QACvB,cAAc,KAAK,EAAE,CAAA;IAEvB,OAAO,CACL,KAAC,OAAO;IACN,8DAA8D;IAC9D,kEAAkE;IAClE,2DAA2D;;QAF3D,8DAA8D;QAC9D,kEAAkE;QAClE,2DAA2D;QAC3D,SAAS,EAAE,SAAS,IAAI,CAAC,UAAU,EACnC,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,KAAK,EACpB,OAAO,EACL,KAAC,MAAM,OACD,WAAW,EACf,UAAU,EAAE,UAAU,gBACV,GAAG,KAAK,KAAK,WAAW,EAAE,EACtC,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,SAAS,CACV,EACD,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,CAAC,UAAU,EAAE,EAAE;gBACtB,WAAW,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAA;gBAEjC,MAAM,EAAE,CAAA;YACV,CAAC,EACD,IAAI,EAAE,IAAI,EACV,IAAI,EAAC,QAAQ,YAEZ,iBAAiB,CAAC,CAAC,CAAC,CAOnB,eAAM,SAAS,EAAC,oFAAoF,YACjG,cAAc,GACV,CACR,CAAC,CAAC,CAAC,IAAI,GACD,GAEX,CACH,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
CHANGED
package/src/Picker/Picker.mdx
CHANGED
|
@@ -77,6 +77,20 @@ all flush; they are all the same trap.
|
|
|
77
77
|
|
|
78
78
|
<Canvas of={PickerStories.NextToText} />
|
|
79
79
|
|
|
80
|
+
## A stretched trigger reads like a select
|
|
81
|
+
|
|
82
|
+
`Picker` is a `Button`, and a `Button` centers its content. Inside a `Field` — a `flex-col`,
|
|
83
|
+
so the trigger stretches to the column — or under `w-full`, that used to give a full-width bar
|
|
84
|
+
with the value floating in the middle. The trigger now lays its value out at the **inline
|
|
85
|
+
start** and the chevron at the **inline end**, and a value wider than the trigger truncates
|
|
86
|
+
with an ellipsis rather than pushing the chevron out. The full value stays in the accessible
|
|
87
|
+
name and in the open list.
|
|
88
|
+
|
|
89
|
+
The width still comes from the container: a trigger the parent does not stretch keeps its
|
|
90
|
+
content width and renders exactly as before.
|
|
91
|
+
|
|
92
|
+
<Canvas of={PickerStories.Responsive} />
|
|
93
|
+
|
|
80
94
|
## `id` does not survive; `data-testid` does
|
|
81
95
|
|
|
82
96
|
`useAnchoredOverlay` overwrites the trigger's `id` with a generated one so the portalled
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../argTypes.storyHelpers.ts"
|
|
11
11
|
import { Badge } from "../Badge/Badge.tsx"
|
|
12
12
|
import {
|
|
13
|
+
ContainerBoard,
|
|
13
14
|
StoryRow,
|
|
14
15
|
StorySection,
|
|
15
16
|
} from "../board.storyHelpers.tsx"
|
|
@@ -258,3 +259,48 @@ export const NextToText: Story = {
|
|
|
258
259
|
</div>
|
|
259
260
|
),
|
|
260
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One value long enough to outgrow the narrowest container, so the
|
|
265
|
+
* board shows the truncation as well as the stretch.
|
|
266
|
+
*/
|
|
267
|
+
const SUBTITLES: PickerOption[] = [
|
|
268
|
+
{ label: "Off", value: "off" },
|
|
269
|
+
{
|
|
270
|
+
label: "English (SDH, signs and songs, full dialogue)",
|
|
271
|
+
value: "eng-sdh",
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Inside a `Field` the trigger stretches to the container, the way a
|
|
277
|
+
* select does: the value sits at the inline start, the chevron at the
|
|
278
|
+
* inline end, and a value wider than the container truncates rather
|
|
279
|
+
* than pushing the chevron out.
|
|
280
|
+
*/
|
|
281
|
+
export const Responsive: Story = {
|
|
282
|
+
render: () => (
|
|
283
|
+
<ContainerBoard>
|
|
284
|
+
{(width) => (
|
|
285
|
+
<div className="flex flex-col gap-4">
|
|
286
|
+
<Field label={`Language at ${width}`}>
|
|
287
|
+
<Picker
|
|
288
|
+
label={`Language at ${width}`}
|
|
289
|
+
onChange={() => undefined}
|
|
290
|
+
options={LANGUAGES}
|
|
291
|
+
value="eng"
|
|
292
|
+
/>
|
|
293
|
+
</Field>
|
|
294
|
+
<Field label={`Subtitles at ${width}`}>
|
|
295
|
+
<Picker
|
|
296
|
+
label={`Subtitles at ${width}`}
|
|
297
|
+
onChange={() => undefined}
|
|
298
|
+
options={SUBTITLES}
|
|
299
|
+
value="eng-sdh"
|
|
300
|
+
/>
|
|
301
|
+
</Field>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</ContainerBoard>
|
|
305
|
+
),
|
|
306
|
+
}
|
|
@@ -7,8 +7,13 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
7
7
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
8
|
import * as stories from "./Picker.stories.tsx"
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
|
|
10
|
+
const {
|
|
11
|
+
AllStates,
|
|
12
|
+
Playground,
|
|
13
|
+
Interactive,
|
|
14
|
+
NextToText,
|
|
15
|
+
Responsive,
|
|
16
|
+
} = composeStories(stories)
|
|
12
17
|
|
|
13
18
|
test("the trigger's accessible name carries the current value", async () => {
|
|
14
19
|
const { canvas, canvasElement } =
|
|
@@ -108,3 +113,87 @@ test("a rich option label keeps a plain-string trigger via textValue", async ()
|
|
|
108
113
|
|
|
109
114
|
await expectNoAxeViolations(canvasElement)
|
|
110
115
|
})
|
|
116
|
+
|
|
117
|
+
test("a stretched trigger puts the value at the start and the chevron at the end", async () => {
|
|
118
|
+
const { canvas, canvasElement } =
|
|
119
|
+
await mountStory(Responsive)
|
|
120
|
+
|
|
121
|
+
const trigger = expectAgentDrivable(canvas, {
|
|
122
|
+
name: "Language at 34rem: English",
|
|
123
|
+
role: "button",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const valueElement = canvas
|
|
127
|
+
.getAllByText("English")
|
|
128
|
+
.find((element: HTMLElement) =>
|
|
129
|
+
trigger.contains(element),
|
|
130
|
+
) as HTMLElement
|
|
131
|
+
|
|
132
|
+
// The text's own box, not the span's: the span grows to fill the
|
|
133
|
+
// trigger, so its box would sit at the start even with centered text.
|
|
134
|
+
const valueRange = document.createRange()
|
|
135
|
+
|
|
136
|
+
valueRange.selectNodeContents(valueElement)
|
|
137
|
+
|
|
138
|
+
const valueBox = valueRange.getBoundingClientRect()
|
|
139
|
+
const triggerBox = trigger.getBoundingClientRect()
|
|
140
|
+
const chevronBox = (
|
|
141
|
+
trigger.querySelector("svg") as SVGElement
|
|
142
|
+
).getBoundingClientRect()
|
|
143
|
+
|
|
144
|
+
const triggerStyle = getComputedStyle(trigger)
|
|
145
|
+
const contentStart =
|
|
146
|
+
triggerBox.left +
|
|
147
|
+
Number.parseFloat(triggerStyle.borderInlineStartWidth) +
|
|
148
|
+
Number.parseFloat(triggerStyle.paddingInlineStart)
|
|
149
|
+
const contentEnd =
|
|
150
|
+
triggerBox.right -
|
|
151
|
+
Number.parseFloat(triggerStyle.borderInlineEndWidth) -
|
|
152
|
+
Number.parseFloat(triggerStyle.paddingInlineEnd)
|
|
153
|
+
|
|
154
|
+
// The `Field` stretched the trigger well past its content, so a
|
|
155
|
+
// centered value would start far from the inline start.
|
|
156
|
+
await expect(
|
|
157
|
+
triggerBox.width - valueBox.width - chevronBox.width,
|
|
158
|
+
).toBeGreaterThan(200)
|
|
159
|
+
await expect(
|
|
160
|
+
Math.abs(valueBox.left - contentStart),
|
|
161
|
+
).toBeLessThan(1)
|
|
162
|
+
await expect(
|
|
163
|
+
Math.abs(chevronBox.right - contentEnd),
|
|
164
|
+
).toBeLessThan(1)
|
|
165
|
+
|
|
166
|
+
await expectNoAxeViolations(canvasElement)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("a value wider than the trigger truncates and keeps the chevron inside", async () => {
|
|
170
|
+
const { canvas } = await mountStory(Responsive)
|
|
171
|
+
|
|
172
|
+
const trigger = expectAgentDrivable(canvas, {
|
|
173
|
+
name: "Subtitles at 15rem: English (SDH, signs and songs, full dialogue)",
|
|
174
|
+
role: "button",
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const valueElement = canvas
|
|
178
|
+
.getAllByText(
|
|
179
|
+
"English (SDH, signs and songs, full dialogue)",
|
|
180
|
+
)
|
|
181
|
+
.find((element: HTMLElement) =>
|
|
182
|
+
trigger.contains(element),
|
|
183
|
+
) as HTMLElement
|
|
184
|
+
|
|
185
|
+
const triggerBox = trigger.getBoundingClientRect()
|
|
186
|
+
const chevronBox = (
|
|
187
|
+
trigger.querySelector("svg") as SVGElement
|
|
188
|
+
).getBoundingClientRect()
|
|
189
|
+
|
|
190
|
+
await expect(valueElement.scrollWidth).toBeGreaterThan(
|
|
191
|
+
valueElement.clientWidth,
|
|
192
|
+
)
|
|
193
|
+
await expect(trigger.scrollWidth).toBe(
|
|
194
|
+
trigger.clientWidth,
|
|
195
|
+
)
|
|
196
|
+
await expect(chevronBox.right).toBeLessThanOrEqual(
|
|
197
|
+
triggerBox.right,
|
|
198
|
+
)
|
|
199
|
+
})
|
package/src/Picker/Picker.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { Button } from "../Button/Button.tsx"
|
|
|
13
13
|
import type { IntentAppearance } from "../intentStyles.ts"
|
|
14
14
|
import type { ListboxItem } from "../Listbox/Listbox.tsx"
|
|
15
15
|
import { Listbox } from "../Listbox/Listbox.tsx"
|
|
16
|
+
import { toClassName } from "../toClassName.ts"
|
|
16
17
|
|
|
17
18
|
export type PickerOption = ListboxItem
|
|
18
19
|
|
|
@@ -122,6 +123,21 @@ export type PickerProps = Omit<
|
|
|
122
123
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
123
124
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
124
125
|
* `onClick`, `form`, …) passes straight through.
|
|
126
|
+
*
|
|
127
|
+
* ### A stretched trigger reads like a select, not a button
|
|
128
|
+
*
|
|
129
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
130
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
131
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
132
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
133
|
+
* at the inline start, in a span that grows into the spare width and
|
|
134
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
135
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
136
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
137
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
138
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
139
|
+
* at its content width has no spare width to distribute, so it renders
|
|
140
|
+
* exactly as it did before.
|
|
125
141
|
*/
|
|
126
142
|
export const Picker = ({
|
|
127
143
|
appearance = "outline",
|
|
@@ -154,6 +170,18 @@ export const Picker = ({
|
|
|
154
170
|
|
|
155
171
|
const triggerText = currentText ?? placeholder ?? ""
|
|
156
172
|
|
|
173
|
+
const triggerContent =
|
|
174
|
+
currentOption === undefined
|
|
175
|
+
? placeholder
|
|
176
|
+
: currentOption.label
|
|
177
|
+
|
|
178
|
+
// No content, no span: an empty flex item would still earn a `gap`
|
|
179
|
+
// before the chevron and widen a content-width trigger.
|
|
180
|
+
const hasTriggerContent =
|
|
181
|
+
triggerContent !== undefined &&
|
|
182
|
+
triggerContent !== null &&
|
|
183
|
+
triggerContent !== ""
|
|
184
|
+
|
|
157
185
|
return (
|
|
158
186
|
<Listbox
|
|
159
187
|
// A disabled control must not open, and hiding the panel is a
|
|
@@ -170,7 +198,10 @@ export const Picker = ({
|
|
|
170
198
|
{...buttonProps}
|
|
171
199
|
appearance={appearance}
|
|
172
200
|
aria-label={`${label}: ${triggerText}`}
|
|
173
|
-
className={
|
|
201
|
+
className={toClassName(
|
|
202
|
+
"justify-between",
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
174
205
|
iconEnd={iconEnd}
|
|
175
206
|
intent={intent}
|
|
176
207
|
isDisabled={isDisabled}
|
|
@@ -182,9 +213,17 @@ export const Picker = ({
|
|
|
182
213
|
size={size}
|
|
183
214
|
type="button"
|
|
184
215
|
>
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
{hasTriggerContent ? (
|
|
217
|
+
// Not `truncate`. Its `overflow: hidden` clips the
|
|
218
|
+
// anti-aliased edge pixels of glyphs and a rich label's
|
|
219
|
+
// badge, so a content-width trigger stopped rendering
|
|
220
|
+
// pixel-identically to the unwrapped one. `overflow-x-clip`
|
|
221
|
+
// leaves the block axis alone, and `-mx-1 px-1` moves the
|
|
222
|
+
// inline clip edge 4px outward without moving the text.
|
|
223
|
+
<span className="-mx-1 min-w-0 grow overflow-x-clip px-1 text-start text-ellipsis whitespace-nowrap">
|
|
224
|
+
{triggerContent}
|
|
225
|
+
</span>
|
|
226
|
+
) : null}
|
|
188
227
|
</Button>
|
|
189
228
|
}
|
|
190
229
|
/>
|