@colixsystems/widget-sdk 0.247.0 → 0.249.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/README.md +76 -6
- package/dist/board-auto-scroll.js +59 -0
- package/dist/board-view.js +327 -0
- package/dist/contract.cjs +76 -4
- package/dist/contract.js +76 -4
- package/dist/data-table-logic.js +179 -0
- package/dist/data-table-view.js +388 -0
- package/dist/data-table.js +22 -0
- package/dist/data-table.native.js +14 -0
- package/dist/drag-kit-view.js +310 -0
- package/dist/file-upload-view.js +13 -2
- package/dist/filter-bar-logic.js +143 -0
- package/dist/filter-bar-view.js +381 -0
- package/dist/filter-bar.js +15 -0
- package/dist/filter-bar.native.js +13 -0
- package/dist/form-control-logic.js +20 -3
- package/dist/form-control-view.js +18 -0
- package/dist/form-layout-logic.js +92 -0
- package/dist/form-view.js +268 -0
- package/dist/form.js +15 -0
- package/dist/form.native.js +9 -0
- package/dist/index.d.ts +402 -8
- package/dist/index.js +8 -0
- package/dist/index.native.js +8 -0
- package/dist/linter.cjs +156 -0
- package/dist/linter.js +156 -0
- package/dist/menu-view.js +5 -2
- package/dist/people-picker-view.js +3 -0
- package/dist/radio-group-view.js +7 -3
- package/dist/search-field-tokens.js +4 -6
- package/dist/search-field-view.js +43 -123
- package/dist/select-view.js +7 -2
- package/dist/sortable-list-view.js +228 -0
- package/dist/sortable-logic.js +273 -0
- package/dist/sortable-tokens.js +120 -0
- package/dist/sortable-view.js +24 -0
- package/dist/sortable.js +39 -0
- package/dist/sortable.native.js +30 -0
- package/dist/text-field-adornment.js +93 -0
- package/dist/text-field-view.js +109 -10
- package/package.json +1 -1
- package/src/contract.js +76 -4
package/dist/linter.cjs
CHANGED
|
@@ -1563,6 +1563,160 @@ function _fullWidthMarginRules(source) {
|
|
|
1563
1563
|
return findings;
|
|
1564
1564
|
}
|
|
1565
1565
|
|
|
1566
|
+
// sc-12232 — a field inside a bordered row draws two borders.
|
|
1567
|
+
// A warning: refs resolve by key name, and a bordered column is fine.
|
|
1568
|
+
const _FIELD_TAG_NAMES = ["TextField", "SearchField"];
|
|
1569
|
+
const _JSX_TOKEN_RE = /<(\/?)([A-Z][\w.]*)?/g;
|
|
1570
|
+
const _STYLE_ATTR_RE = /\bstyle\s*=\s*\{/;
|
|
1571
|
+
const _STYLE_REF_RE = /\b[A-Za-z_$][\w$]*\.([A-Za-z_$][\w$]*)/g;
|
|
1572
|
+
const _STYLE_ENTRY_RE = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*:\s*\{/g;
|
|
1573
|
+
const _BORDER_WIDTH_KEY_RE = /\bborderWidth\s*:/g;
|
|
1574
|
+
const _ROW_KEY_RE = /\bflexDirection\s*:\s*(["'`])row(?:-reverse)?\1/;
|
|
1575
|
+
const _UNNAMED_PRESS_RE = /\bonTrailingPress\s*=/;
|
|
1576
|
+
const _PRESS_NAME_RE = /\btrailingAccessibilityLabel\s*=|\{\s*\.\.\./;
|
|
1577
|
+
|
|
1578
|
+
// One pass over the source: the first `key: {` of each name, by offset.
|
|
1579
|
+
function _styleEntryOffsets(structure) {
|
|
1580
|
+
const offsets = new Map();
|
|
1581
|
+
_STYLE_ENTRY_RE.lastIndex = 0;
|
|
1582
|
+
let entry;
|
|
1583
|
+
while ((entry = _STYLE_ENTRY_RE.exec(structure))) {
|
|
1584
|
+
if (!offsets.has(entry[1])) offsets.set(entry[1], entry.index + entry[0].length);
|
|
1585
|
+
}
|
|
1586
|
+
return offsets;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// The object literals a style expression spans, plus the entries it names.
|
|
1590
|
+
function _styleObjectSpans(scan, [from, to]) {
|
|
1591
|
+
const { structure } = scan;
|
|
1592
|
+
const spans = [];
|
|
1593
|
+
for (let i = from + 1; i < to - 1; i++) {
|
|
1594
|
+
if (structure[i] !== "{") continue;
|
|
1595
|
+
const span = _enclosingObject(structure, i + 1);
|
|
1596
|
+
if (!span) continue;
|
|
1597
|
+
spans.push(span);
|
|
1598
|
+
i = span[1] - 1;
|
|
1599
|
+
}
|
|
1600
|
+
if (!scan.entries) scan.entries = _styleEntryOffsets(structure);
|
|
1601
|
+
const keys = new Set();
|
|
1602
|
+
_STYLE_REF_RE.lastIndex = 0;
|
|
1603
|
+
const refs = structure.slice(from, to);
|
|
1604
|
+
let ref;
|
|
1605
|
+
while ((ref = _STYLE_REF_RE.exec(refs))) keys.add(ref[1]);
|
|
1606
|
+
for (const key of keys) {
|
|
1607
|
+
const at = scan.entries.get(key);
|
|
1608
|
+
const span = at === undefined ? null : _enclosingObject(structure, at);
|
|
1609
|
+
if (span) spans.push(span);
|
|
1610
|
+
}
|
|
1611
|
+
return spans;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function _isBorderedRow(scan, tag) {
|
|
1615
|
+
const { code, structure } = scan;
|
|
1616
|
+
const attrs = structure.slice(tag.attrs[0], tag.attrs[1]);
|
|
1617
|
+
const style = _STYLE_ATTR_RE.exec(attrs);
|
|
1618
|
+
if (!style) return false;
|
|
1619
|
+
const open = tag.attrs[0] + style.index + style[0].length - 1;
|
|
1620
|
+
const inner = _bracedSpan(structure, open);
|
|
1621
|
+
if (inner === null) return false;
|
|
1622
|
+
let row = false;
|
|
1623
|
+
let bordered = false;
|
|
1624
|
+
// `inner` excludes its two braces.
|
|
1625
|
+
for (const span of _styleObjectSpans(scan, [open, open + inner.length + 2])) {
|
|
1626
|
+
const keys = _ownKeysMask(structure, span);
|
|
1627
|
+
if (_ROW_KEY_RE.test(_ownKeysMask(code, span))) row = true;
|
|
1628
|
+
_BORDER_WIDTH_KEY_RE.lastIndex = 0;
|
|
1629
|
+
let key;
|
|
1630
|
+
while ((key = _BORDER_WIDTH_KEY_RE.exec(keys))) {
|
|
1631
|
+
const valueAt = span[0] + key.index + key[0].length;
|
|
1632
|
+
if (!_ZERO_VALUE_RE.test(code.slice(valueAt, valueAt + 40))) bordered = true;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return row && bordered;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function _selfClosing(structure, end) {
|
|
1639
|
+
let i = end - 1;
|
|
1640
|
+
while (i >= 0 && /\s/.test(structure[i])) i--;
|
|
1641
|
+
return structure[i] === "/";
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function _fieldInBorderedRowRules(source) {
|
|
1645
|
+
const findings = [];
|
|
1646
|
+
const scan = {
|
|
1647
|
+
code: _stripNonCode(source, { keepStrings: true }),
|
|
1648
|
+
structure: _stripNonCode(source),
|
|
1649
|
+
entries: null,
|
|
1650
|
+
};
|
|
1651
|
+
const { code, structure } = scan;
|
|
1652
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1653
|
+
// Tokens arrive in source order, so one running count serves every finding.
|
|
1654
|
+
let line = 1;
|
|
1655
|
+
let counted = 0;
|
|
1656
|
+
const lineOf = (at) => {
|
|
1657
|
+
for (; counted < at; counted++) if (code[counted] === "\n") line++;
|
|
1658
|
+
return line;
|
|
1659
|
+
};
|
|
1660
|
+
const report = (rule, at, label) => {
|
|
1661
|
+
const n = lineOf(at);
|
|
1662
|
+
findings.push({
|
|
1663
|
+
rule,
|
|
1664
|
+
severity: "warning",
|
|
1665
|
+
label,
|
|
1666
|
+
line: n,
|
|
1667
|
+
snippet: (sourceLines[n - 1] || "").trim().slice(0, 200),
|
|
1668
|
+
});
|
|
1669
|
+
};
|
|
1670
|
+
const open = [];
|
|
1671
|
+
_JSX_TOKEN_RE.lastIndex = 0;
|
|
1672
|
+
let token;
|
|
1673
|
+
while ((token = _JSX_TOKEN_RE.exec(structure))) {
|
|
1674
|
+
const [, closing, name] = token;
|
|
1675
|
+
if (!name) {
|
|
1676
|
+
// A fragment, `<>` or `</>`; any other `<` is a comparison.
|
|
1677
|
+
if (structure[token.index + 1 + closing.length] !== ">") continue;
|
|
1678
|
+
if (closing) open.pop();
|
|
1679
|
+
else open.push(null);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (closing) {
|
|
1683
|
+
const at = open.map((tag) => tag && tag.name).lastIndexOf(name);
|
|
1684
|
+
if (at >= 0) open.length = at;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
const end = _tagEnd(structure, token.index);
|
|
1688
|
+
if (end === -1) break;
|
|
1689
|
+
const tag = { name, attrs: [token.index + token[0].length, end], borderedRow: undefined };
|
|
1690
|
+
if (_FIELD_TAG_NAMES.includes(name)) {
|
|
1691
|
+
const parent = open[open.length - 1];
|
|
1692
|
+
if (parent && parent.borderedRow === undefined) parent.borderedRow = _isBorderedRow(scan, parent);
|
|
1693
|
+
if (parent && parent.borderedRow) {
|
|
1694
|
+
report(
|
|
1695
|
+
"field-in-bordered-row",
|
|
1696
|
+
token.index,
|
|
1697
|
+
`<${name}> draws its own border, so the bordered row around it ` +
|
|
1698
|
+
`shows a box inside a box. Put the icon in the field with ` +
|
|
1699
|
+
`leadingIcon / trailingIcon (onTrailingPress makes it a button) ` +
|
|
1700
|
+
`and drop the row's border.`,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const attrs = structure.slice(tag.attrs[0], tag.attrs[1]);
|
|
1704
|
+
if (_UNNAMED_PRESS_RE.test(attrs) && !_PRESS_NAME_RE.test(attrs)) {
|
|
1705
|
+
report(
|
|
1706
|
+
"field-trailing-press-unnamed",
|
|
1707
|
+
token.index,
|
|
1708
|
+
`onTrailingPress makes the trailing icon a button with no name, so ` +
|
|
1709
|
+
`a screen reader says only "button". Give it a translated ` +
|
|
1710
|
+
`trailingAccessibilityLabel.`,
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (!_selfClosing(structure, end)) open.push(tag);
|
|
1715
|
+
_JSX_TOKEN_RE.lastIndex = end + 1;
|
|
1716
|
+
}
|
|
1717
|
+
return findings;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1566
1720
|
// sc-4913 — a measured frame INCLUDES the element's own padding, so cells sized
|
|
1567
1721
|
// from it overflow the content box and the last one wraps into an empty column.
|
|
1568
1722
|
// Mirror of linter.js (see there for the full rationale).
|
|
@@ -2188,6 +2342,8 @@ function lintSource(source, options) {
|
|
|
2188
2342
|
findings.push(..._flexBasisPercentRules(source));
|
|
2189
2343
|
// sc-11277 — soft warning: a full-width box with a side margin is clipped.
|
|
2190
2344
|
findings.push(..._fullWidthMarginRules(source));
|
|
2345
|
+
// sc-12232 — soft warning: a field inside a bordered row draws two borders.
|
|
2346
|
+
findings.push(..._fieldInBorderedRowRules(source));
|
|
2191
2347
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
2192
2348
|
// padding wraps the last grid column into an empty one.
|
|
2193
2349
|
findings.push(..._measuredPaddingRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -1604,6 +1604,160 @@ function _fullWidthMarginRules(source) {
|
|
|
1604
1604
|
return findings;
|
|
1605
1605
|
}
|
|
1606
1606
|
|
|
1607
|
+
// sc-12232 — a field inside a bordered row draws two borders.
|
|
1608
|
+
// A warning: refs resolve by key name, and a bordered column is fine.
|
|
1609
|
+
const _FIELD_TAG_NAMES = ["TextField", "SearchField"];
|
|
1610
|
+
const _JSX_TOKEN_RE = /<(\/?)([A-Z][\w.]*)?/g;
|
|
1611
|
+
const _STYLE_ATTR_RE = /\bstyle\s*=\s*\{/;
|
|
1612
|
+
const _STYLE_REF_RE = /\b[A-Za-z_$][\w$]*\.([A-Za-z_$][\w$]*)/g;
|
|
1613
|
+
const _STYLE_ENTRY_RE = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*:\s*\{/g;
|
|
1614
|
+
const _BORDER_WIDTH_KEY_RE = /\bborderWidth\s*:/g;
|
|
1615
|
+
const _ROW_KEY_RE = /\bflexDirection\s*:\s*(["'`])row(?:-reverse)?\1/;
|
|
1616
|
+
const _UNNAMED_PRESS_RE = /\bonTrailingPress\s*=/;
|
|
1617
|
+
const _PRESS_NAME_RE = /\btrailingAccessibilityLabel\s*=|\{\s*\.\.\./;
|
|
1618
|
+
|
|
1619
|
+
// One pass over the source: the first `key: {` of each name, by offset.
|
|
1620
|
+
function _styleEntryOffsets(structure) {
|
|
1621
|
+
const offsets = new Map();
|
|
1622
|
+
_STYLE_ENTRY_RE.lastIndex = 0;
|
|
1623
|
+
let entry;
|
|
1624
|
+
while ((entry = _STYLE_ENTRY_RE.exec(structure))) {
|
|
1625
|
+
if (!offsets.has(entry[1])) offsets.set(entry[1], entry.index + entry[0].length);
|
|
1626
|
+
}
|
|
1627
|
+
return offsets;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// The object literals a style expression spans, plus the entries it names.
|
|
1631
|
+
function _styleObjectSpans(scan, [from, to]) {
|
|
1632
|
+
const { structure } = scan;
|
|
1633
|
+
const spans = [];
|
|
1634
|
+
for (let i = from + 1; i < to - 1; i++) {
|
|
1635
|
+
if (structure[i] !== "{") continue;
|
|
1636
|
+
const span = _enclosingObject(structure, i + 1);
|
|
1637
|
+
if (!span) continue;
|
|
1638
|
+
spans.push(span);
|
|
1639
|
+
i = span[1] - 1;
|
|
1640
|
+
}
|
|
1641
|
+
if (!scan.entries) scan.entries = _styleEntryOffsets(structure);
|
|
1642
|
+
const keys = new Set();
|
|
1643
|
+
_STYLE_REF_RE.lastIndex = 0;
|
|
1644
|
+
const refs = structure.slice(from, to);
|
|
1645
|
+
let ref;
|
|
1646
|
+
while ((ref = _STYLE_REF_RE.exec(refs))) keys.add(ref[1]);
|
|
1647
|
+
for (const key of keys) {
|
|
1648
|
+
const at = scan.entries.get(key);
|
|
1649
|
+
const span = at === undefined ? null : _enclosingObject(structure, at);
|
|
1650
|
+
if (span) spans.push(span);
|
|
1651
|
+
}
|
|
1652
|
+
return spans;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function _isBorderedRow(scan, tag) {
|
|
1656
|
+
const { code, structure } = scan;
|
|
1657
|
+
const attrs = structure.slice(tag.attrs[0], tag.attrs[1]);
|
|
1658
|
+
const style = _STYLE_ATTR_RE.exec(attrs);
|
|
1659
|
+
if (!style) return false;
|
|
1660
|
+
const open = tag.attrs[0] + style.index + style[0].length - 1;
|
|
1661
|
+
const inner = _bracedSpan(structure, open);
|
|
1662
|
+
if (inner === null) return false;
|
|
1663
|
+
let row = false;
|
|
1664
|
+
let bordered = false;
|
|
1665
|
+
// `inner` excludes its two braces.
|
|
1666
|
+
for (const span of _styleObjectSpans(scan, [open, open + inner.length + 2])) {
|
|
1667
|
+
const keys = _ownKeysMask(structure, span);
|
|
1668
|
+
if (_ROW_KEY_RE.test(_ownKeysMask(code, span))) row = true;
|
|
1669
|
+
_BORDER_WIDTH_KEY_RE.lastIndex = 0;
|
|
1670
|
+
let key;
|
|
1671
|
+
while ((key = _BORDER_WIDTH_KEY_RE.exec(keys))) {
|
|
1672
|
+
const valueAt = span[0] + key.index + key[0].length;
|
|
1673
|
+
if (!_ZERO_VALUE_RE.test(code.slice(valueAt, valueAt + 40))) bordered = true;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return row && bordered;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function _selfClosing(structure, end) {
|
|
1680
|
+
let i = end - 1;
|
|
1681
|
+
while (i >= 0 && /\s/.test(structure[i])) i--;
|
|
1682
|
+
return structure[i] === "/";
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function _fieldInBorderedRowRules(source) {
|
|
1686
|
+
const findings = [];
|
|
1687
|
+
const scan = {
|
|
1688
|
+
code: _stripNonCode(source, { keepStrings: true }),
|
|
1689
|
+
structure: _stripNonCode(source),
|
|
1690
|
+
entries: null,
|
|
1691
|
+
};
|
|
1692
|
+
const { code, structure } = scan;
|
|
1693
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1694
|
+
// Tokens arrive in source order, so one running count serves every finding.
|
|
1695
|
+
let line = 1;
|
|
1696
|
+
let counted = 0;
|
|
1697
|
+
const lineOf = (at) => {
|
|
1698
|
+
for (; counted < at; counted++) if (code[counted] === "\n") line++;
|
|
1699
|
+
return line;
|
|
1700
|
+
};
|
|
1701
|
+
const report = (rule, at, label) => {
|
|
1702
|
+
const n = lineOf(at);
|
|
1703
|
+
findings.push({
|
|
1704
|
+
rule,
|
|
1705
|
+
severity: "warning",
|
|
1706
|
+
label,
|
|
1707
|
+
line: n,
|
|
1708
|
+
snippet: (sourceLines[n - 1] || "").trim().slice(0, 200),
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
const open = [];
|
|
1712
|
+
_JSX_TOKEN_RE.lastIndex = 0;
|
|
1713
|
+
let token;
|
|
1714
|
+
while ((token = _JSX_TOKEN_RE.exec(structure))) {
|
|
1715
|
+
const [, closing, name] = token;
|
|
1716
|
+
if (!name) {
|
|
1717
|
+
// A fragment, `<>` or `</>`; any other `<` is a comparison.
|
|
1718
|
+
if (structure[token.index + 1 + closing.length] !== ">") continue;
|
|
1719
|
+
if (closing) open.pop();
|
|
1720
|
+
else open.push(null);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (closing) {
|
|
1724
|
+
const at = open.map((tag) => tag && tag.name).lastIndexOf(name);
|
|
1725
|
+
if (at >= 0) open.length = at;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const end = _tagEnd(structure, token.index);
|
|
1729
|
+
if (end === -1) break;
|
|
1730
|
+
const tag = { name, attrs: [token.index + token[0].length, end], borderedRow: undefined };
|
|
1731
|
+
if (_FIELD_TAG_NAMES.includes(name)) {
|
|
1732
|
+
const parent = open[open.length - 1];
|
|
1733
|
+
if (parent && parent.borderedRow === undefined) parent.borderedRow = _isBorderedRow(scan, parent);
|
|
1734
|
+
if (parent && parent.borderedRow) {
|
|
1735
|
+
report(
|
|
1736
|
+
"field-in-bordered-row",
|
|
1737
|
+
token.index,
|
|
1738
|
+
`<${name}> draws its own border, so the bordered row around it ` +
|
|
1739
|
+
`shows a box inside a box. Put the icon in the field with ` +
|
|
1740
|
+
`leadingIcon / trailingIcon (onTrailingPress makes it a button) ` +
|
|
1741
|
+
`and drop the row's border.`,
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
const attrs = structure.slice(tag.attrs[0], tag.attrs[1]);
|
|
1745
|
+
if (_UNNAMED_PRESS_RE.test(attrs) && !_PRESS_NAME_RE.test(attrs)) {
|
|
1746
|
+
report(
|
|
1747
|
+
"field-trailing-press-unnamed",
|
|
1748
|
+
token.index,
|
|
1749
|
+
`onTrailingPress makes the trailing icon a button with no name, so ` +
|
|
1750
|
+
`a screen reader says only "button". Give it a translated ` +
|
|
1751
|
+
`trailingAccessibilityLabel.`,
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (!_selfClosing(structure, end)) open.push(tag);
|
|
1756
|
+
_JSX_TOKEN_RE.lastIndex = end + 1;
|
|
1757
|
+
}
|
|
1758
|
+
return findings;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1607
1761
|
// sc-4913 — a measured frame INCLUDES the element's own padding.
|
|
1608
1762
|
//
|
|
1609
1763
|
// `onLayout` reports the frame width, and padding sits inside that frame. A
|
|
@@ -2256,6 +2410,8 @@ export function lintSource(source, options) {
|
|
|
2256
2410
|
findings.push(..._flexBasisPercentRules(source));
|
|
2257
2411
|
// sc-11277 — soft warning: a full-width box with a side margin is clipped.
|
|
2258
2412
|
findings.push(..._fullWidthMarginRules(source));
|
|
2413
|
+
// sc-12232 — soft warning: a field inside a bordered row draws two borders.
|
|
2414
|
+
findings.push(..._fieldInBorderedRowRules(source));
|
|
2259
2415
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
2260
2416
|
// padding wraps the last grid column into an empty one.
|
|
2261
2417
|
findings.push(..._measuredPaddingRules(source));
|
package/dist/menu-view.js
CHANGED
|
@@ -140,8 +140,11 @@ export function makeMenu(rn, AnchoredSurface) {
|
|
|
140
140
|
null,
|
|
141
141
|
triggerWith(trigger, {
|
|
142
142
|
anchorRef,
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// An empty menu never opens: expanded with nothing shown misleads a reader.
|
|
144
|
+
onOpen: () => {
|
|
145
|
+
if (list.length > 0) setOpen(true);
|
|
146
|
+
},
|
|
147
|
+
extraProps: { "aria-haspopup": "menu", "aria-expanded": open && list.length > 0 },
|
|
145
148
|
}),
|
|
146
149
|
h(
|
|
147
150
|
AnchoredSurface,
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
settleLookup,
|
|
32
32
|
unresolvedIds,
|
|
33
33
|
} from "./people-picker-logic.js";
|
|
34
|
+
import { DRAWS_FIELD_FRAME } from "./form-control-logic.js";
|
|
34
35
|
|
|
35
36
|
const h = React.createElement;
|
|
36
37
|
const NO_ROWS = Object.freeze([]);
|
|
@@ -243,5 +244,7 @@ export function makePeoplePicker(rn) {
|
|
|
243
244
|
});
|
|
244
245
|
|
|
245
246
|
PeoplePicker.displayName = "PeoplePicker";
|
|
247
|
+
// Its `<Select>` frames the label, so `required` passes straight through.
|
|
248
|
+
PeoplePicker[DRAWS_FIELD_FRAME] = true;
|
|
246
249
|
return PeoplePicker;
|
|
247
250
|
}
|
package/dist/radio-group-view.js
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// an icon or a tab role. This one has the radio role and the app's look.
|
|
7
7
|
|
|
8
8
|
import React from "react";
|
|
9
|
-
import { useHostTheme } from "./hooks.js";
|
|
9
|
+
import { useHostSharedText, useHostTheme } from "./hooks.js";
|
|
10
10
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
11
11
|
import { Icon } from "./icon.js";
|
|
12
12
|
import { CHECKBOX_SIZE, CONTROL_MIN_TARGET, focusHaloStyle } from "./form-control-tokens.js";
|
|
13
13
|
import {
|
|
14
|
+
DRAWS_FIELD_FRAME,
|
|
14
15
|
errorMessage,
|
|
15
16
|
hasNote,
|
|
16
17
|
isShown,
|
|
@@ -165,11 +166,13 @@ export function makeRadioGroup(rn) {
|
|
|
165
166
|
helper,
|
|
166
167
|
error,
|
|
167
168
|
disabled = false,
|
|
169
|
+
required = false,
|
|
168
170
|
accessibilityLabel,
|
|
169
171
|
style,
|
|
170
172
|
containerStyle,
|
|
171
173
|
...rest
|
|
172
174
|
} = props;
|
|
175
|
+
const requiredWord = useHostSharedText("required", "Required");
|
|
173
176
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
174
177
|
const t = resolveRadioGroupTokens(theme);
|
|
175
178
|
const kind = radioGroupVariant(variant);
|
|
@@ -236,7 +239,7 @@ export function makeRadioGroup(rn) {
|
|
|
236
239
|
...rest,
|
|
237
240
|
ref,
|
|
238
241
|
accessibilityRole: "radiogroup",
|
|
239
|
-
accessibilityLabel: spokenLabel(accessibilityLabel, label),
|
|
242
|
+
accessibilityLabel: spokenLabel(accessibilityLabel, label, required ? requiredWord : null),
|
|
240
243
|
"aria-disabled": disabled,
|
|
241
244
|
...(invalid ? { "aria-invalid": true } : null),
|
|
242
245
|
...(noted ? { "aria-describedby": noteId } : null),
|
|
@@ -252,11 +255,12 @@ export function makeRadioGroup(rn) {
|
|
|
252
255
|
if (!framed) return group;
|
|
253
256
|
return h(
|
|
254
257
|
FieldFrame,
|
|
255
|
-
{ t, label, helper, error: errorMessage(error), disabled, noteId, style: containerStyle },
|
|
258
|
+
{ t, label, helper, error: errorMessage(error), disabled, required, noteId, style: containerStyle },
|
|
256
259
|
group,
|
|
257
260
|
);
|
|
258
261
|
});
|
|
259
262
|
|
|
260
263
|
RadioGroup.displayName = "RadioGroup";
|
|
264
|
+
RadioGroup[DRAWS_FIELD_FRAME] = true;
|
|
261
265
|
return RadioGroup;
|
|
262
266
|
}
|
|
@@ -55,19 +55,17 @@ export function resolveSearchFieldTokens(theme, own) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* What fills `<TextField>`'s two icon slots. At the end, the clear control
|
|
59
59
|
* takes the glyph's slot while there is text, so the two never overlap.
|
|
60
60
|
*
|
|
61
61
|
* @param {string} iconPosition — a resolved SEARCH_ICON_POSITIONS value.
|
|
62
62
|
* @param {boolean} clearable — the field has text and is enabled.
|
|
63
|
-
* @returns {{ glyph: "
|
|
63
|
+
* @returns {{ leading: "glyph"|null, trailing: "glyph"|"clear"|null }}
|
|
64
64
|
*/
|
|
65
65
|
export function searchFieldLayout(iconPosition, clearable) {
|
|
66
|
-
const glyphAtEnd = iconPosition === "end" && !clearable;
|
|
67
66
|
return {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
padEnd: clearable || iconPosition === "end",
|
|
67
|
+
leading: iconPosition === "start" ? "glyph" : null,
|
|
68
|
+
trailing: clearable ? "clear" : iconPosition === "end" ? "glyph" : null,
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
|
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
// The ONE implementation behind the `<SearchField>` SDK primitive. It is
|
|
2
|
-
// `<TextField>` with a search glyph and a clear control
|
|
3
|
-
// field and takes the same props, plus the theme's `search`
|
|
4
|
-
// bindings (`search-field.js` / `search-field.native.js`)
|
|
5
|
-
// the primitives come from.
|
|
2
|
+
// `<TextField>` with a search glyph and a clear control in its icon slots, so
|
|
3
|
+
// it wears the same field and takes the same props, plus the theme's `search`
|
|
4
|
+
// look; the platform bindings (`search-field.js` / `search-field.native.js`)
|
|
5
|
+
// differ only in where the primitives come from.
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { useHostTheme } from "./hooks.js";
|
|
9
9
|
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
10
|
-
import { Icon } from "./icon.js";
|
|
11
|
-
import { resolveFormControlTokens } from "./form-control-tokens.js";
|
|
12
10
|
import { resolveSearchFieldTokens, searchFieldLayout } from "./search-field-tokens.js";
|
|
13
|
-
import {
|
|
14
|
-
errorMessage,
|
|
15
|
-
hasNote,
|
|
16
|
-
isShown,
|
|
17
|
-
shareRef,
|
|
18
|
-
spokenLabel,
|
|
19
|
-
} from "./form-control-logic.js";
|
|
20
|
-
import { makeFieldFrame } from "./form-control-view.js";
|
|
11
|
+
import { shareRef, spokenLabel } from "./form-control-logic.js";
|
|
21
12
|
|
|
22
13
|
const h = React.createElement;
|
|
23
|
-
const GLYPH_SIZE = 18;
|
|
24
|
-
// The glyph and the clear control each own a strip this wide inside the field.
|
|
25
|
-
const ADORNMENT_WIDTH = 40;
|
|
26
|
-
// A real target: the web host ignores hitSlop, so size carries it.
|
|
27
|
-
const CLEAR_MIN_SIZE = 36;
|
|
28
|
-
|
|
29
|
-
function adornmentSlot(side) {
|
|
30
|
-
return {
|
|
31
|
-
position: "absolute",
|
|
32
|
-
top: 0,
|
|
33
|
-
bottom: 0,
|
|
34
|
-
alignItems: "center",
|
|
35
|
-
justifyContent: "center",
|
|
36
|
-
...(side === "start" ? { left: 0 } : { right: 0 }),
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
14
|
|
|
40
15
|
/**
|
|
41
16
|
* Bind the shared search field to one platform's primitives.
|
|
@@ -43,8 +18,7 @@ function adornmentSlot(side) {
|
|
|
43
18
|
* @param {{View: any, Text: any, Pressable: any, TextField: any}} rn
|
|
44
19
|
*/
|
|
45
20
|
export function makeSearchField(rn) {
|
|
46
|
-
const {
|
|
47
|
-
const FieldFrame = makeFieldFrame(rn);
|
|
21
|
+
const { TextField } = rn;
|
|
48
22
|
|
|
49
23
|
const SearchField = React.forwardRef(function SearchField(props, ref) {
|
|
50
24
|
const {
|
|
@@ -54,113 +28,59 @@ export function makeSearchField(rn) {
|
|
|
54
28
|
placeholder = "Search",
|
|
55
29
|
clearAccessibilityLabel = "Clear search",
|
|
56
30
|
label,
|
|
57
|
-
helper,
|
|
58
|
-
error,
|
|
59
31
|
disabled = false,
|
|
60
32
|
accessibilityLabel,
|
|
61
33
|
iconPosition,
|
|
62
|
-
style,
|
|
63
|
-
containerStyle,
|
|
64
34
|
testID,
|
|
65
35
|
...rest
|
|
66
36
|
} = props;
|
|
67
37
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
68
|
-
const t = resolveFormControlTokens(theme);
|
|
69
38
|
const search = resolveSearchFieldTokens(theme, { iconPosition });
|
|
70
39
|
const inputRef = React.useRef(null);
|
|
71
40
|
const setRef = React.useMemo(() => shareRef(ref, inputRef), [ref]);
|
|
72
|
-
const noteId = React.useId();
|
|
73
|
-
const focusInput = () => {
|
|
74
|
-
const input = inputRef.current;
|
|
75
|
-
if (input && typeof input.focus === "function") input.focus();
|
|
76
|
-
};
|
|
77
41
|
const hasText = typeof value === "string" && value.length > 0;
|
|
78
|
-
const
|
|
79
|
-
const
|
|
42
|
+
const layout = searchFieldLayout(search.iconPosition, hasText && !disabled);
|
|
43
|
+
const slotTestID = (suffix) => (testID ? `${testID}-${suffix}` : undefined);
|
|
80
44
|
const clear = () => {
|
|
81
45
|
if (typeof onChangeText === "function") onChangeText("");
|
|
82
46
|
if (typeof onClear === "function") onClear();
|
|
83
|
-
|
|
47
|
+
const input = inputRef.current;
|
|
48
|
+
if (input && typeof input.focus === "function") input.focus();
|
|
84
49
|
};
|
|
85
50
|
|
|
86
|
-
return h(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
...search.field,
|
|
120
|
-
...rest,
|
|
121
|
-
style: [
|
|
122
|
-
layout.padStart ? { paddingLeft: ADORNMENT_WIDTH } : null,
|
|
123
|
-
layout.padEnd ? { paddingRight: ADORNMENT_WIDTH } : null,
|
|
124
|
-
style,
|
|
125
|
-
],
|
|
126
|
-
}),
|
|
127
|
-
layout.glyph
|
|
128
|
-
? h(
|
|
129
|
-
View,
|
|
130
|
-
{
|
|
131
|
-
testID: testID ? `${testID}-icon` : undefined,
|
|
132
|
-
style: {
|
|
133
|
-
...adornmentSlot(layout.glyph),
|
|
134
|
-
width: ADORNMENT_WIDTH,
|
|
135
|
-
pointerEvents: "none",
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
|
|
139
|
-
)
|
|
140
|
-
: null,
|
|
141
|
-
clearable
|
|
142
|
-
? h(
|
|
143
|
-
Pressable,
|
|
144
|
-
{
|
|
145
|
-
onPress: clear,
|
|
146
|
-
accessibilityRole: "button",
|
|
147
|
-
accessibilityLabel: clearAccessibilityLabel,
|
|
148
|
-
testID: testID ? `${testID}-clear` : undefined,
|
|
149
|
-
style: ({ pressed }) => [
|
|
150
|
-
{
|
|
151
|
-
...adornmentSlot("end"),
|
|
152
|
-
right: 2,
|
|
153
|
-
minWidth: CLEAR_MIN_SIZE,
|
|
154
|
-
minHeight: CLEAR_MIN_SIZE,
|
|
155
|
-
},
|
|
156
|
-
pressed ? { opacity: 0.6 } : null,
|
|
157
|
-
],
|
|
158
|
-
},
|
|
159
|
-
h(Icon, { name: "x", size: GLYPH_SIZE, color: t.placeholderColor }),
|
|
160
|
-
)
|
|
161
|
-
: null,
|
|
162
|
-
),
|
|
163
|
-
);
|
|
51
|
+
return h(TextField, {
|
|
52
|
+
ref: setRef,
|
|
53
|
+
value,
|
|
54
|
+
onChangeText,
|
|
55
|
+
placeholder,
|
|
56
|
+
label,
|
|
57
|
+
disabled,
|
|
58
|
+
// `role` names it on the web; `accessibilityRole` on a device.
|
|
59
|
+
role: "searchbox",
|
|
60
|
+
accessibilityRole: "search",
|
|
61
|
+
accessibilityLabel: spokenLabel(accessibilityLabel, label) || placeholder,
|
|
62
|
+
enterKeyHint: "search",
|
|
63
|
+
autoCapitalize: "none",
|
|
64
|
+
autoCorrect: false,
|
|
65
|
+
testID,
|
|
66
|
+
// The field's own props beat the theme's search look.
|
|
67
|
+
...search.field,
|
|
68
|
+
...rest,
|
|
69
|
+
// Both slots always passed, so the clear control never rebuilds the input.
|
|
70
|
+
leadingIcon: layout.leading === "glyph" ? "search" : null,
|
|
71
|
+
leadingTestID: slotTestID("icon"),
|
|
72
|
+
...(layout.trailing === "clear"
|
|
73
|
+
? {
|
|
74
|
+
trailingIcon: "x",
|
|
75
|
+
onTrailingPress: clear,
|
|
76
|
+
trailingAccessibilityLabel: clearAccessibilityLabel,
|
|
77
|
+
trailingTestID: slotTestID("clear"),
|
|
78
|
+
}
|
|
79
|
+
: {
|
|
80
|
+
trailingIcon: layout.trailing === "glyph" ? "search" : null,
|
|
81
|
+
trailingTestID: slotTestID("icon"),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
164
84
|
});
|
|
165
85
|
|
|
166
86
|
SearchField.displayName = "SearchField";
|