的实现与无障碍实践)
用 Yeti Affix 组件拼接表单控件共享边框输入组单位、符号、按钮的实现与无障碍实践【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yetiAffix 是 Yeti 中的一个 CSS-first 组件它把输入控件与紧贴它的附加物——货币符号、度量单位、固定域名后缀、操作按钮甚至另一个控件——在视觉上合并为一个整体共享一条边框和同一个高度让两部分读起来像一件事。本文基于 affix 组件文档 展开结合 affix.css、example.html、manifest.json 以及 浏览器测试 源码级讲解其工作原理、完整用法与屏幕阅读器适配方案读完后你可以在表单中直接拼出价格输入、域名输入、国家码电话等常见的粘连控件。何时使用 Affix文档给出的判断标准非常直观只要加入的成员共享一条边框和一个高度能让整体读起来像一个控件就该用 affix。典型的自然场景包括价格与其货币符号$在左¥在右距离与其单位12.5km搜索框与它旁边的搜索按钮URL 与其后面的固定域名my-site.foundationcss.com电话号码旁的国家代码86 电话号码任何控件 属于它的一部分的组合。核心语义是这些附加物不是独立控件而是主控件的一部分。如果附加物和控件各自有边框、各自有高度、中间还隔着间距视觉上就变成了两个互不相干的东西而 affix 通过一框一高让它们黏合成一个整体。从 manifest.json 可以看到组件的结构契约 *至少 2 个成员按视觉顺序排列、至多 2 个span文本附加物、至多 2 个.button按钮附加物任意数量的input/select控件。该组件自 Yeti 7.0.0 起提供。工作原理一条 flex 行与边框重叠affix的实现非常精简全部逻辑在 src/components/affix/affix.css位于layer yeti.components层。逐条拆解.affix { display: flex; align-items: stretch; } .affix * { margin: 0; }容器是一条flex行成员在交叉轴方向stretch因此所有成员天然共享同一个高度margin: 0清掉浏览器默认边距如button的默认外边距为后面的负 margin 重叠做准备。.affix :is(input, select) { flex: 1; min-inline-size: 0; }所有input/select控件flex: 1增长以填满剩余宽度min-inline-size: 0允许它们在小空间内收缩避免 flex 项的最小内容宽度把行撑爆。这是测试中断言控件宽度大于组宽的一半的底层来源。.affix span { display: flex; align-items: center; padding-inline: var(--_yeti-size-space, var(--yeti-space-sm)); font-size: var(--_yeti-size-text, var(--yeti-text-md)); color: var(--yeti-color-text-muted); background-color: var(--yeti-color-surface-sunken); border: var(--yeti-border-width) solid var(--yeti-control-border); border-radius: var(--yeti-control-radius); white-space: nowrap; }span附加物被渲染成一个标签盒子内容垂直居中、水平留白由内联尺寸 token 控制文字用--yeti-color-text-muted弱化、背景用--yeti-color-surface-sunken沉陷表面与可编辑的控件区域区分开边框沿用控件边框--yeti-control-border圆角沿用--yeti-control-radiuswhite-space: nowrap防止单位被折行。注意这里的--_yeti-size-*是带下划线前缀的内部私有变量——这正是读取周围 field 的尺寸的机制稍后详解。.affix .button { border-radius: var(--yeti-control-radius); }作为附加物的.button按钮保留它自己的颜色体系按钮组件本身的强调色、悬停态等只把圆角统一为控件的圆角。.affix :not(:first-child) { border-start-start-radius: 0; border-end-start-radius: 0; margin-inline-start: calc(-1 * var(--yeti-border-width)); } .affix :not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; }这是无缝拼缝的核心第一个成员之后的所有成员左边两个角逻辑属性border-start-start-radius/border-end-start-radius即 LTR 下的左上、左下压平为 0最后一个成员之前的所有成员右边两个角压平为 0。同时每个后续成员通过margin-inline-start: calc(-1 * var(--yeti-border-width))向左回退一个边框宽度让相邻两条边框重叠成一条线。于是组的外角保留圆角、内角全部变直角接缝是一条 1px 的细线而不是两条平行的粗线。.affix :focus-visible { position: relative; z-index: 1; }最后一个细节是焦点管理当任一成员获得键盘焦点:focus-visible时提升到z-index: 1并建立定位上下文让聚焦时的边框/描边浮在相邻成员的边框之上不会被接缝处的重叠边框遮住。这保证了键盘导航时的可辨识度。尺寸从哪里来读取 field 而非自设默认值源码注释明确写着It reads the size of the field around it, so it sets no defaults of its own.它读取周围 field 的尺寸因此不设置自己的默认值。affix 的字体、内边距都取自--_yeti-size-text/--_yeti-size-space而这两个私有变量由外层.field声明——参见 src/components/field/field.css.field:not([data-size]) { --_yeti-size-text: var(--yeti-text-md); --_yeti-size-space: var(--yeti-space-sm); }也就是说在field上设置data-size即可整体缩放整行 affix。浏览器测试 affix.spec.js 中的 reads the size of the field around it 用例正是验证这一点data-sizelg的 field 里affix 附加物的font-size和整行高度都大于默认尺寸的对照行。相应的 fixture 见 test/browser/fixtures/components/affix.html其中#big一行带data-sizelg。底层尺寸值来自全局比例尺 src/tokens/scale.css--yeti-text-md、--yeti-space-sm等都是基于--yeti-base/--yeti-ratio流体比例尺计算出的步骤因此 affix 会随视口与主题平滑缩放。字段相关 token 在 src/tokens/components.css 中声明如--yeti-field-gap。完整用法一前缀 控件 后缀按钮这是 affix 最典型的形态一个货币符号前缀、一个数字输入、一个按钮后缀。完整的可运行示例就是 example.html 中的价格输入div classfield label forpricePrice/label div classaffix span idprice-unit$/span input idprice typenumber min0 step0.01 aria-describedbyprice-unit button classbutton typebutton>div classfield>div classfield label forphonePhone/label div classaffix select aria-labelCountry codeoption1/optionoption44/option/select input idphone typetel /div /divCSS 对select与input一视同仁flex: 1两个控件都会增长、共享同一高度。测试用例 two controls join into one row and both grow 验证了两个控件顶端对齐、接缝处重叠恰好一个边框宽度、两者宽度之和撑满整行、内角为直角。无障碍附加物必须说出口这是 affix 文档重点强调的一节也是实际项目中最容易踩坑的地方。关键原则前缀/后缀不属于控件本身屏幕阅读器不会自动把它和输入值一起读出来。如果你的附加物携带意义货币符号、度量单位必须二选一用aria-describedby关联给span一个id把它放进控件的aria-describedby。这样读屏软件会把附加物作为控件的描述一起读出示例input idsite typetext aria-describedbysite-domain span idsite-domain.foundationcss.com/span写进 label 文本直接把单位写进标签例如 Price in dollars以美元计价的价格让意义从标签传达前缀退化为纯装饰性视觉元素。另外两点必须遵守field 的label仍只指向一个控件for对应那个控件的idaffix 里出现第二个控件时它必须有自己的aria-label例如国家代码下拉框的aria-labelCountry code。manifest.json 的a11y字段把这些规则固化为组件的契约accessible name 或 description 必须覆盖有意义的附加物多控件时每个控件都要可命名affix 中的按钮就是普通按钮。此外测试套件中的 axe 无障碍扫描has no accessibility violations以及 light/dark 两种配色方案下的 AA 对比度检查--yeti-color-text-muted在沉陷表面上都在 affix.spec.js 中持续回归验证。定制令牌affix 对外暴露的定制点全部是公开令牌定义见 manifest.json 的tokens段也都在 affix.css 中直接使用令牌作用默认来源--yeti-control-radius组的外角圆角全局控件圆角主题可改如 sharp/soft 主题--yeti-control-border附加物span的边框颜色全局控件边框色--yeti-border-width成员互相重叠的宽度即接缝线宽全局边框宽度由于它们都回落到全局基础 token见 src/tokens/color.css 与 src/tokens/components.css只改主题、不碰组件就能让所有 affix 同步变化——这与 Yeti CSS-first、零构建 的设计一致不需要任何 JavaScript引入 src/yeti.css 后直接写类名即可。与 Field 组件的协作Affix 的定位是field 的控件槽位manifest 中明确写道 Usually the affix is the control slot of a field。两者协作带来几个实战红利尺寸联动affix 不自设尺寸data-size写在 field 上整行含附加物一起缩放错误联动field 的校验态会穿透到 affix 内部——field.css 中有:has( .affix :is(:user-invalid, [aria-invalidtrue]))规则当 affix 内的控件通过:user-invalid用户触碰过且非法或服务端回填的aria-invalidtrue触发校验时整个组的边框会变为告警色--yeti-color-alert同时 field 的[data-error]提示文案显示出来label 语义完整label → field → affix → 控件的层级让for/id的关联保持清晰配合aria-describedby即可同时送达帮助文本、错误文本与附加物描述。完整的多控件、多尺寸、校验联动的可运行 fixture 可在 test/browser/fixtures/components/affix.html 中查看浏览器测试 test/browser/components/affix.spec.js 则用几何断言元素矩形、边框宽度 token、圆角像素值从数学上保证了共享一条边框、共享一个高度、内角为直角、控件撑满剩余宽度这些核心承诺。小结Affix 以不到 40 行 CSS 完成了表单领域最常见的粘连输入需求一条 flex 行 负边距边框重叠 逻辑属性圆角裁剪配合 field 的尺寸注入实现了零配置、主题一致、随 field 缩放的输入组而aria-describedby、多控件aria-label与:focus-visible提层则让它在键盘导航和屏幕阅读器下同样可靠。需要深入了解组件在完整文档体系中的位置可参阅 组件指南 与 field 组件文档。【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yeti创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考