@scalewing/react 1.13.0 → 1.14.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/CHANGELOG.md +6 -0
- package/README.md +12 -0
- package/dist/components/Select.d.ts +13 -1
- package/dist/components/Select.js +2 -2
- package/dist/css/css-select.js +33 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +32 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 84d5fd9: `Select` takes `width?: 'content' | 'full'` (`docs/requests/teisoro-select.md`, 2026-09-29 follow-up). `'content'`, the default, is today's field, as wide as its longest option; nothing changes for existing Selects. `'full'` adds the generated `sw-select-full` class: the field and its trigger fill the container's inline size in a `Stack`, a `Grid` cell or a narrow column, and in an `Inline` row they take the space the siblings leave (`.sw-inline > .sw-select-full { flex: 1 1 0; min-width: 0 }`), so a `Button` beside it keeps its label on one line. The value still ellipsizes, the chevron stays at the inline end (left to right and right to left), and the open list is exactly the trigger's width, so a long option wraps inside it, even a single long word (`overflow-wrap: anywhere`), instead of running past a phone's edge or scrolling sideways. New exported type `SelectWidth`. No new dependencies.
|
|
8
|
+
|
|
3
9
|
## 1.13.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -78,6 +78,18 @@ A disabled native text control (`<input>`, `<select>` or `<textarea>` on the can
|
|
|
78
78
|
/>
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
+
`Select` `width` is `'content'` (the default: the field is as wide as its longest option, as above) or `'full'`: the field and its trigger fill the container's inline size in a `Stack`, a `Grid` cell or any narrow column, such as a filter above full-width cards on a phone. In an `Inline` row it takes the space its siblings leave, so a `Button` beside it keeps its label on one line. The value still takes the free space and ellipsizes past it, the chevron stays at the inline end (the left, right to left), and the open list is exactly the trigger's width, so it never runs past the screen's edge; a long option wraps inside it, even a single long word.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Select
|
|
85
|
+
label="Show"
|
|
86
|
+
onChange={setFilter}
|
|
87
|
+
options={filters}
|
|
88
|
+
value={filter}
|
|
89
|
+
width="full"
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
81
93
|
`Accordion` is a native `<details>` disclosure controlled by `open` and `onOpenChange`. A token chevron at the end of the header replaces the browser triangle and turns when it opens (it holds still under reduced motion). `subtitle` adds one muted caption line under the title, such as a summary of what the section holds; it wraps rather than truncating and is read after the title. `size="sm"` is a quieter disclosure nested inside other content: a label-size title, tighter padding, smaller corners and a header that is an `sm` control (at least 32px).
|
|
82
94
|
|
|
83
95
|
```tsx
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type SelectOption } from '../select-list.js';
|
|
2
2
|
import { type FieldSize } from './Field.js';
|
|
3
3
|
export type { SelectOption };
|
|
4
|
+
/**
|
|
5
|
+
* `content` (the default) sizes the field to its longest option, as a native
|
|
6
|
+
* select is; `full` fills the container's inline size (in an Inline row, the
|
|
7
|
+
* space its siblings leave).
|
|
8
|
+
*/
|
|
9
|
+
export type SelectWidth = 'content' | 'full';
|
|
4
10
|
export type SelectAction = {
|
|
5
11
|
label: string;
|
|
6
12
|
onPress: () => void;
|
|
@@ -22,5 +28,11 @@ export type SelectProps = {
|
|
|
22
28
|
required?: boolean;
|
|
23
29
|
/** A validation message under the control, wired as Field's error is. */
|
|
24
30
|
error?: string;
|
|
31
|
+
/**
|
|
32
|
+
* `full` makes the field, its trigger and its open list fill the
|
|
33
|
+
* container's inline size; the chosen label still ellipsizes past it.
|
|
34
|
+
* Default `content`.
|
|
35
|
+
*/
|
|
36
|
+
width?: SelectWidth;
|
|
25
37
|
};
|
|
26
|
-
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, }: SelectProps): import("react").JSX.Element;
|
|
38
|
+
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, width, }: SelectProps): import("react").JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { SelectListbox } from './select/SelectListbox.js';
|
|
|
9
9
|
import { SelectValue } from './select/SelectValue.js';
|
|
10
10
|
import { useSelectList } from './select/use-select-list.js';
|
|
11
11
|
import { Stack } from './Stack.js';
|
|
12
|
-
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, }) {
|
|
12
|
+
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, width = 'content', }) {
|
|
13
13
|
if (placeholder !== undefined && !placeholder.trim())
|
|
14
14
|
throw new RangeError('placeholder must not be empty');
|
|
15
15
|
const labelId = useId();
|
|
@@ -22,7 +22,7 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
|
|
|
22
22
|
const showsPlaceholder = placeholder !== undefined &&
|
|
23
23
|
!options.some((option) => option.value === value);
|
|
24
24
|
const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
|
|
25
|
-
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
25
|
+
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
26
26
|
const next = event.relatedTarget;
|
|
27
27
|
if (next instanceof Node && rootRef.current?.contains(next)) {
|
|
28
28
|
return;
|
package/dist/css/css-select.js
CHANGED
|
@@ -9,6 +9,25 @@ export function cssSelectClasses() {
|
|
|
9
9
|
width: max-content;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/*
|
|
13
|
+
* width="full": the field and its trigger fill the container's inline size.
|
|
14
|
+
* The value still takes the free space and ellipsizes, and the chevron stays
|
|
15
|
+
* last, at the inline end. In an Inline row the field takes only the space
|
|
16
|
+
* its siblings leave, so a Button beside it keeps its label on one line.
|
|
17
|
+
*/
|
|
18
|
+
.sw-select-full {
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.sw-inline > .sw-select-full {
|
|
23
|
+
flex: 1 1 0;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sw-select-full .sw-select-trigger {
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
.sw-select-control {
|
|
13
32
|
position: relative;
|
|
14
33
|
}
|
|
@@ -125,6 +144,15 @@ export function cssSelectClasses() {
|
|
|
125
144
|
${zIndex('popup')}
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
/*
|
|
148
|
+
* A full-width field's open list is exactly the trigger's width (min-width
|
|
149
|
+
* already beats the dialog-max cap), so it never runs past a phone's edge;
|
|
150
|
+
* a long option wraps inside it, even a single long word.
|
|
151
|
+
*/
|
|
152
|
+
.sw-select-full .sw-select-list {
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
.sw-select-option {
|
|
129
157
|
border-radius: var(--sw-radius-sm);
|
|
130
158
|
box-sizing: border-box;
|
|
@@ -135,6 +163,10 @@ export function cssSelectClasses() {
|
|
|
135
163
|
padding-inline: var(--sw-control-xs-padding-inline);
|
|
136
164
|
}
|
|
137
165
|
|
|
166
|
+
.sw-select-full .sw-select-option {
|
|
167
|
+
overflow-wrap: anywhere;
|
|
168
|
+
}
|
|
169
|
+
|
|
138
170
|
.sw-select-option[aria-selected='true'] {
|
|
139
171
|
font-weight: ${label.fontWeight};
|
|
140
172
|
}
|
|
@@ -159,6 +191,7 @@ export function selectClassCatalog() {
|
|
|
159
191
|
return [
|
|
160
192
|
'sw-select',
|
|
161
193
|
'sw-select-xs',
|
|
194
|
+
'sw-select-full',
|
|
162
195
|
'sw-select-control',
|
|
163
196
|
'sw-select-trigger',
|
|
164
197
|
'sw-select-value',
|
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { SplitProps } from './components/Split.js';
|
|
|
29
29
|
export { Nav, type NavProps } from './components/Nav.js';
|
|
30
30
|
export { SegmentedControl, type SegmentedControlProps, type SegmentedControlVariant, type SegmentedItem, } from './components/SegmentedControl.js';
|
|
31
31
|
export { Select } from './components/Select.js';
|
|
32
|
-
export type { SelectAction, SelectOption, SelectProps, } from './components/Select.js';
|
|
32
|
+
export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
|
|
33
33
|
export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
|
|
34
34
|
export { Table, TableBody, TableCell, TableHeader, TableRow, type TableBodyProps, type TableCellProps, type TableDensity, type TableHeaderProps, type TableProps, type TableRowProps, } from './components/Table.js';
|
|
35
35
|
export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
|
package/dist/styles.css
CHANGED
|
@@ -1912,6 +1912,25 @@ body {
|
|
|
1912
1912
|
width: max-content;
|
|
1913
1913
|
}
|
|
1914
1914
|
|
|
1915
|
+
/*
|
|
1916
|
+
* width="full": the field and its trigger fill the container's inline size.
|
|
1917
|
+
* The value still takes the free space and ellipsizes, and the chevron stays
|
|
1918
|
+
* last, at the inline end. In an Inline row the field takes only the space
|
|
1919
|
+
* its siblings leave, so a Button beside it keeps its label on one line.
|
|
1920
|
+
*/
|
|
1921
|
+
.sw-select-full {
|
|
1922
|
+
width: 100%;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
.sw-inline > .sw-select-full {
|
|
1926
|
+
flex: 1 1 0;
|
|
1927
|
+
min-width: 0;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
.sw-select-full .sw-select-trigger {
|
|
1931
|
+
width: 100%;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1915
1934
|
.sw-select-control {
|
|
1916
1935
|
position: relative;
|
|
1917
1936
|
}
|
|
@@ -2032,6 +2051,15 @@ body {
|
|
|
2032
2051
|
z-index: 10;
|
|
2033
2052
|
}
|
|
2034
2053
|
|
|
2054
|
+
/*
|
|
2055
|
+
* A full-width field's open list is exactly the trigger's width (min-width
|
|
2056
|
+
* already beats the dialog-max cap), so it never runs past a phone's edge;
|
|
2057
|
+
* a long option wraps inside it, even a single long word.
|
|
2058
|
+
*/
|
|
2059
|
+
.sw-select-full .sw-select-list {
|
|
2060
|
+
width: 100%;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2035
2063
|
.sw-select-option {
|
|
2036
2064
|
border-radius: var(--sw-radius-sm);
|
|
2037
2065
|
box-sizing: border-box;
|
|
@@ -2042,6 +2070,10 @@ body {
|
|
|
2042
2070
|
padding-inline: var(--sw-control-xs-padding-inline);
|
|
2043
2071
|
}
|
|
2044
2072
|
|
|
2073
|
+
.sw-select-full .sw-select-option {
|
|
2074
|
+
overflow-wrap: anywhere;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2045
2077
|
.sw-select-option[aria-selected='true'] {
|
|
2046
2078
|
font-weight: 600;
|
|
2047
2079
|
}
|