ember-primitives 0.62.0 → 0.63.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/declarations/components/accordion/content.d.ts +2 -1
  3. package/declarations/components/accordion/content.d.ts.map +1 -1
  4. package/declarations/components/accordion/header.d.ts +2 -1
  5. package/declarations/components/accordion/header.d.ts.map +1 -1
  6. package/declarations/components/accordion/item.d.ts +7 -0
  7. package/declarations/components/accordion/item.d.ts.map +1 -1
  8. package/declarations/components/accordion/public.d.ts +3 -3
  9. package/declarations/components/accordion/public.d.ts.map +1 -1
  10. package/declarations/components/accordion/trigger.d.ts +2 -1
  11. package/declarations/components/accordion/trigger.d.ts.map +1 -1
  12. package/declarations/components/command-palette.d.ts +13 -0
  13. package/declarations/components/command-palette.d.ts.map +1 -1
  14. package/declarations/components/dialog.d.ts +14 -6
  15. package/declarations/components/dialog.d.ts.map +1 -1
  16. package/declarations/components/drawer.d.ts +14 -6
  17. package/declarations/components/drawer.d.ts.map +1 -1
  18. package/declarations/components/link.d.ts.map +1 -1
  19. package/declarations/components/menu.d.ts.map +1 -1
  20. package/declarations/components/one-time-password/otp.d.ts +1 -1
  21. package/declarations/components/one-time-password/utils.d.ts.map +1 -1
  22. package/declarations/components/rating/range.d.ts +1 -0
  23. package/declarations/components/rating/range.d.ts.map +1 -1
  24. package/declarations/components/rating/rating.d.ts +1 -1
  25. package/declarations/components/rating/rating.d.ts.map +1 -1
  26. package/declarations/components/rating/stars.d.ts.map +1 -1
  27. package/declarations/components/rating/state.d.ts +1 -0
  28. package/declarations/components/rating/state.d.ts.map +1 -1
  29. package/declarations/components/slider.d.ts +2 -0
  30. package/declarations/components/slider.d.ts.map +1 -1
  31. package/declarations/components/switch.d.ts +5 -1
  32. package/declarations/components/switch.d.ts.map +1 -1
  33. package/declarations/components/tabs.d.ts +30 -1
  34. package/declarations/components/tabs.d.ts.map +1 -1
  35. package/declarations/components/zoetrope/index.d.ts.map +1 -1
  36. package/dist/components/accordion.js +12 -4
  37. package/dist/components/accordion.js.map +1 -1
  38. package/dist/components/command-palette.js +8 -1
  39. package/dist/components/command-palette.js.map +1 -1
  40. package/dist/components/dialog.js +9 -2
  41. package/dist/components/dialog.js.map +1 -1
  42. package/dist/components/drawer.js +9 -2
  43. package/dist/components/drawer.js.map +1 -1
  44. package/dist/components/link.js +1 -1
  45. package/dist/components/link.js.map +1 -1
  46. package/dist/components/menu.js +5 -1
  47. package/dist/components/menu.js.map +1 -1
  48. package/dist/components/one-time-password.js +1 -1
  49. package/dist/components/rating.js +1 -1
  50. package/dist/components/slider.js +6 -1
  51. package/dist/components/switch.js +1 -1
  52. package/dist/components/switch.js.map +1 -1
  53. package/dist/components/tabs.js +18 -5
  54. package/dist/components/tabs.js.map +1 -1
  55. package/dist/components/zoetrope/styles.css +6 -1
  56. package/dist/components/zoetrope.js +1 -1
  57. package/dist/{index-gRO4Cvlf.js → index-B-yrfUe9.js} +10 -3
  58. package/dist/index-B-yrfUe9.js.map +1 -0
  59. package/dist/index.js +3 -3
  60. package/dist/{otp-H5a4B4yu.js → otp-C8c1EIw6.js} +27 -15
  61. package/dist/otp-C8c1EIw6.js.map +1 -0
  62. package/dist/{rating-BrIiwDLw.js → rating-CKWhrr-L.js} +9 -5
  63. package/dist/rating-CKWhrr-L.js.map +1 -0
  64. package/package.json +1 -1
  65. package/src/components/accordion/content.gts +4 -2
  66. package/src/components/accordion/header.gts +8 -2
  67. package/src/components/accordion/item.gts +18 -2
  68. package/src/components/accordion/public.ts +12 -3
  69. package/src/components/accordion/trigger.gts +5 -3
  70. package/src/components/command-palette.gts +22 -0
  71. package/src/components/dialog.gts +15 -3
  72. package/src/components/drawer.gts +15 -3
  73. package/src/components/link.gts +1 -0
  74. package/src/components/menu.gts +7 -0
  75. package/src/components/one-time-password/input.gts +1 -1
  76. package/src/components/one-time-password/otp.gts +1 -1
  77. package/src/components/one-time-password/utils.ts +26 -13
  78. package/src/components/rating/range.gts +3 -0
  79. package/src/components/rating/rating.gts +7 -2
  80. package/src/components/rating/stars.gts +0 -1
  81. package/src/components/rating/state.gts +9 -0
  82. package/src/components/slider.gts +8 -0
  83. package/src/components/switch.gts +5 -2
  84. package/src/components/tabs.gts +35 -4
  85. package/src/components/zoetrope/index.gts +12 -3
  86. package/src/components/zoetrope/styles.css +6 -1
  87. package/dist/index-gRO4Cvlf.js.map +0 -1
  88. package/dist/otp-H5a4B4yu.js.map +0 -1
  89. package/dist/rating-BrIiwDLw.js.map +0 -1
@@ -10,7 +10,7 @@ import { assert } from '@ember/debug';
10
10
  import { localCopy } from 'tracked-toolbox';
11
11
  import { g, i, n } from 'decorator-transforms/runtime';
12
12
 
13
- const RatingRange = setComponentTemplate(precompileTemplate("<input ...attributes name={{@name}} type=\"range\" max={{@max}} value={{@value}} {{on \"change\" @handleChange}} />", {
13
+ const RatingRange = setComponentTemplate(precompileTemplate("<input min=\"0\" step={{@step}} ...attributes name={{@name}} type=\"range\" max={{@max}} value={{@value}} {{on \"change\" @handleChange}} />", {
14
14
  strictMode: true,
15
15
  scope: () => ({
16
16
  on
@@ -24,7 +24,7 @@ function lte(a, b) {
24
24
  return a <= b;
25
25
  }
26
26
 
27
- const Stars = setComponentTemplate(precompileTemplate("<div class=\"ember-primitives__rating__items\">\n {{#each @stars as |star|}}\n {{#let (uniqueId) as |id|}}\n <span class=\"ember-primitives__rating__item\" data-number={{star}} data-selected={{lte star @currentValue}} data-readonly={{@isReadonly}}>\n <label for=\"input-{{id}}\">\n <span visually-hidden>{{star}} star</span>\n {{#if @icon}}\n <span aria-hidden=\"true\">\n {{#if (isString @icon)}}\n {{@icon}}\n {{else}}\n <@icon @value={{star}} @isSelected={{lte star @currentValue}} @readonly={{@isReadonly}} />\n {{/if}}\n </span>\n {{/if}}\n </label>\n\n <input id=\"input-{{id}}\" type=\"radio\" name={{@name}} value={{star}} readonly={{@isReadonly}} checked={{Object.is star @currentValue}} />\n </span>\n {{/let}}\n {{/each}}\n</div>", {
27
+ const Stars = setComponentTemplate(precompileTemplate("<div class=\"ember-primitives__rating__items\">\n {{#each @stars as |star|}}\n {{#let (uniqueId) as |id|}}\n <span class=\"ember-primitives__rating__item\" data-number={{star}} data-selected={{lte star @currentValue}} data-readonly={{@isReadonly}}>\n <label for=\"input-{{id}}\">\n <span visually-hidden>{{star}} star</span>\n {{#if @icon}}\n <span aria-hidden=\"true\">\n {{#if (isString @icon)}}\n {{@icon}}\n {{else}}\n <@icon @value={{star}} @isSelected={{lte star @currentValue}} @readonly={{@isReadonly}} />\n {{/if}}\n </span>\n {{/if}}\n </label>\n\n <input id=\"input-{{id}}\" type=\"radio\" name={{@name}} value={{star}} checked={{Object.is star @currentValue}} />\n </span>\n {{/let}}\n {{/each}}\n</div>", {
28
28
  strictMode: true,
29
29
  scope: () => ({
30
30
  uniqueId,
@@ -94,6 +94,10 @@ class RatingState extends Component {
94
94
  // we don't do anything on other elements
95
95
  const isValid = event.target instanceof HTMLInputElement && event.target.name === this.args.name && event.target.type === "radio";
96
96
  if (!isValid) return;
97
+ if (this.args.readonly) {
98
+ /** `readonly` has no effect on a radio, cancelling is what restores the checked one. */event.preventDefault();
99
+ return;
100
+ }
97
101
  const selected = event.target?.value;
98
102
  this.setFromString(selected);
99
103
  };
@@ -107,7 +111,7 @@ class RatingState extends Component {
107
111
  this.setFromString(event.target.value);
108
112
  };
109
113
  static {
110
- setComponentTemplate(precompileTemplate("{{yield (hash stars=this.stars total=this.stars.length handleClick=this.handleClick handleChange=this.handleChange setRating=this.setRating value=this.value step=this.step) (hash total=this.stars.length value=this.value)}}", {
114
+ setComponentTemplate(precompileTemplate("{{yield (hash stars=this.stars max=this.max total=this.stars.length handleClick=this.handleClick handleChange=this.handleChange setRating=this.setRating value=this.value step=this.step) (hash total=this.stars.length value=this.value)}}", {
111
115
  strictMode: true,
112
116
  scope: () => ({
113
117
  hash
@@ -135,7 +139,7 @@ class Rating extends Component {
135
139
  return !this.isInteractive;
136
140
  }
137
141
  static {
138
- setComponentTemplate(precompileTemplate("<RatingState @max={{@max}} @step={{@step}} @value={{@value}} @name={{this.name}} @readonly={{this.isReadonly}} @onChange={{@onChange}} as |r publicState|>\n <fieldset class=\"ember-primitives__rating\" data-total={{r.total}} data-value={{r.value}} data-readonly={{this.isReadonly}} {{!-- We use event delegation, this isn't a primary interactive -- we're capturing events from inputs --}} {{!-- template-lint-disable no-invalid-interactive --}} {{on \"click\" r.handleClick}} ...attributes>\n {{#let (component Stars stars=r.stars icon=this.icon isReadonly=this.isReadonly name=this.name total=r.total currentValue=r.value) as |RatingStars|}}\n\n {{#if (has-block)}}\n {{yield (hash max=r.total total=r.total value=r.value name=this.name isReadonly=this.isReadonly isChangeable=this.isChangeable Stars=RatingStars Range=(component RatingRange step=r.step max=r.total value=r.value name=this.name handleChange=r.handleChange))}}\n {{else}}\n {{#if this.needsDescription}}\n {{#if (has-block \"label\")}}\n {{yield publicState to=\"label\"}}\n {{else}}\n <span visually-hidden class=\"ember-primitives__rating__label\">Rated\n {{r.value}}\n out of\n {{r.total}}</span>\n {{/if}}\n {{else}}\n {{#if (has-block \"label\")}}\n <legend>\n {{yield publicState to=\"label\"}}\n </legend>\n {{/if}}\n {{/if}}\n\n <RatingStars />\n {{/if}}\n {{/let}}\n\n </fieldset>\n</RatingState>", {
142
+ setComponentTemplate(precompileTemplate("<RatingState @max={{@max}} @step={{@step}} @value={{@value}} @name={{this.name}} @readonly={{this.isReadonly}} @onChange={{@onChange}} as |r publicState|>\n <fieldset class=\"ember-primitives__rating\" role=\"radiogroup\" aria-readonly={{if this.isReadonly \"true\"}} data-total={{r.total}} data-value={{r.value}} data-readonly={{this.isReadonly}} {{!-- We use event delegation, this isn't a primary interactive -- we're capturing events from inputs --}} {{!-- template-lint-disable no-invalid-interactive --}} {{on \"click\" r.handleClick}} ...attributes>\n {{#let (component Stars stars=r.stars icon=this.icon isReadonly=this.isReadonly name=this.name total=r.total currentValue=r.value) as |RatingStars|}}\n\n {{#if (has-block)}}\n {{yield (hash max=r.total total=r.total value=r.value name=this.name isReadonly=this.isReadonly isChangeable=this.isChangeable Stars=RatingStars Range=(component RatingRange step=r.step max=r.max value=r.value name=this.name handleChange=r.handleChange))}}\n {{else}}\n {{#if this.needsDescription}}\n {{#if (has-block \"label\")}}\n {{yield publicState to=\"label\"}}\n {{else}}\n <span visually-hidden class=\"ember-primitives__rating__label\">Rated\n {{r.value}}\n out of\n {{r.total}}</span>\n {{/if}}\n {{else}}\n {{#if (has-block \"label\")}}\n <legend>\n {{yield publicState to=\"label\"}}\n </legend>\n {{/if}}\n {{/if}}\n\n <RatingStars />\n {{/if}}\n {{/let}}\n\n </fieldset>\n</RatingState>", {
139
143
  strictMode: true,
140
144
  scope: () => ({
141
145
  RatingState,
@@ -149,4 +153,4 @@ class Rating extends Component {
149
153
  }
150
154
 
151
155
  export { Rating as R };
152
- //# sourceMappingURL=rating-BrIiwDLw.js.map
156
+ //# sourceMappingURL=rating-CKWhrr-L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rating-CKWhrr-L.js","sources":["../src/components/rating/range.gts","../src/components/rating/utils.ts","../src/components/rating/stars.gts","../src/components/rating/state.gts","../src/components/rating/rating.gts"],"sourcesContent":["import { on } from \"@ember/modifier\";\n\nimport type { TOC } from \"@ember/component/template-only\";\n\nexport const RatingRange: TOC<{\n Element: HTMLInputElement;\n Args: {\n name: string;\n step: number;\n max: number;\n value: number;\n handleChange: (event: Event) => void;\n };\n}> = <template>\n <input\n min=\"0\"\n step={{@step}}\n ...attributes\n name={{@name}}\n type=\"range\"\n max={{@max}}\n value={{@value}}\n {{on \"change\" @handleChange}}\n />\n</template>;\n","export function isString(x: unknown) {\n return typeof x === 'string';\n}\n\nexport function lte(a: number, b: number) {\n return a <= b;\n}\n","import { uniqueId } from \"../../utils.ts\";\nimport { isString, lte } from \"./utils.ts\";\n\nimport type { ComponentIcons, StringIcons } from \"./public-types.ts\";\nimport type { TOC } from \"@ember/component/template-only\";\n\nexport const Stars: TOC<{\n Args: {\n // Configuration\n stars: number[];\n icon: StringIcons[\"icon\"] | ComponentIcons[\"icon\"];\n isReadonly: boolean;\n\n // HTML Boilerplate\n name: string;\n\n // State\n currentValue: number;\n total: number;\n };\n}> = <template>\n <div class=\"ember-primitives__rating__items\">\n {{#each @stars as |star|}}\n {{#let (uniqueId) as |id|}}\n <span\n class=\"ember-primitives__rating__item\"\n data-number={{star}}\n data-selected={{lte star @currentValue}}\n data-readonly={{@isReadonly}}\n >\n <label for=\"input-{{id}}\">\n <span visually-hidden>{{star}} star</span>\n {{#if @icon}}\n <span aria-hidden=\"true\">\n {{#if (isString @icon)}}\n {{@icon}}\n {{else}}\n <@icon\n @value={{star}}\n @isSelected={{lte star @currentValue}}\n @readonly={{@isReadonly}}\n />\n {{/if}}\n </span>\n {{/if}}\n </label>\n\n <input\n id=\"input-{{id}}\"\n type=\"radio\"\n name={{@name}}\n value={{star}}\n checked={{Object.is star @currentValue}}\n />\n </span>\n {{/let}}\n {{/each}}\n </div>\n</template>;\n","import Component from \"@glimmer/component\";\nimport { cached } from \"@glimmer/tracking\";\nimport { assert } from \"@ember/debug\";\nimport { hash } from \"@ember/helper\";\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error\nimport { localCopy } from \"tracked-toolbox\";\n\nexport class RatingState extends Component<{\n Args: {\n max: number | undefined;\n value: number | undefined;\n step: number | undefined;\n readonly: boolean | undefined;\n name: string;\n onChange?: (value: number) => void;\n };\n Blocks: {\n default: [\n internalApi: {\n stars: number[];\n step: number;\n value: number;\n max: number;\n total: number;\n handleClick: (event: Event) => void;\n handleChange: (event: Event) => void;\n setRating: (num: number) => void;\n },\n publicApi: {\n value: number;\n total: number;\n },\n ];\n };\n}> {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-call\n @localCopy(\"args.value\") declare _value: number;\n\n get value() {\n return this._value ?? 0;\n }\n\n get step() {\n return this.args.step ?? 1;\n }\n\n get max() {\n return this.args.max ?? 5;\n }\n\n @cached\n get stars() {\n const result = [];\n\n // 0 is \"none selected\"\n let current = 0;\n\n current += this.step;\n\n while (current <= this.max) {\n result.push(current);\n current += this.step;\n }\n\n return result;\n }\n\n setRating = (value: number) => {\n if (this.args.readonly) {\n return;\n }\n\n if (value === this._value) {\n this._value = 0;\n } else {\n this._value = value;\n }\n\n this.args.onChange?.(value);\n };\n\n setFromString = (value: unknown) => {\n assert(\"[BUG]: value from input must be a string.\", typeof value === \"string\");\n\n const num = parseFloat(value);\n\n if (isNaN(num)) {\n // something went wrong.\n // Since we're using event delegation,\n // this could be from an unrelated input\n return;\n }\n\n this.setRating(num);\n };\n\n /**\n * Click events are captured by\n * - radio changes (mouse and keyboard)\n * - but only range clicks\n */\n handleClick = (event: Event) => {\n // Since we're doing event delegation on a click, we want to make sure\n // we don't do anything on other elements\n const isValid =\n event.target instanceof HTMLInputElement &&\n event.target.name === this.args.name &&\n event.target.type === \"radio\";\n\n if (!isValid) return;\n\n if (this.args.readonly) {\n /** `readonly` has no effect on a radio, cancelling is what restores the checked one. */\n event.preventDefault();\n\n return;\n }\n\n const selected = event.target?.value;\n\n this.setFromString(selected);\n };\n\n /**\n * Only attached to a range element, if present.\n * Range elements don't fire click events on keyboard usage, like radios do\n */\n handleChange = (event: Event) => {\n const isValid = event.target !== null && \"value\" in event.target;\n\n if (!isValid) return;\n\n this.setFromString(event.target.value);\n };\n\n <template>\n {{yield\n (hash\n stars=this.stars\n max=this.max\n total=this.stars.length\n handleClick=this.handleClick\n handleChange=this.handleChange\n setRating=this.setRating\n value=this.value\n step=this.step\n )\n (hash total=this.stars.length value=this.value)\n }}\n </template>\n}\n","import Component from \"@glimmer/component\";\nimport { hash } from \"@ember/helper\";\nimport { on } from \"@ember/modifier\";\n\nimport { uniqueId } from \"../../utils.ts\";\nimport { RatingRange } from \"./range.gts\";\nimport { Stars } from \"./stars.gts\";\nimport { RatingState } from \"./state.gts\";\n\nimport type { ComponentIcons, StringIcons } from \"./public-types.ts\";\nimport type { WithBoundArgs } from \"@glint/template\";\n\nexport interface Signature {\n /*\n * The element all passed attributes / modifiers are applied to.\n *\n * This is a `<fieldset>`, becaues the rating elements are\n * powered by a group of radio buttons.\n */\n Element: HTMLFieldSetElement;\n Args: (ComponentIcons | StringIcons) & {\n /**\n * The number of stars/whichever-icon to show\n *\n * Defaults to 5\n */\n max?: number;\n\n /**\n * The current number of stars/whichever-icon to show as selected\n *\n * Defaults to 0\n */\n value?: number;\n\n /**\n * When generating the radio inputs, this changes what value of rating each radio\n * input will be incremented by.\n *\n * e.g.: Set to 0.5 for half-star ratings.\n *\n * Defaults to 1\n */\n step?: number;\n\n /**\n * Prevents click events on the icons and sets aria-readonly.\n *\n * Also sets data-readonly=true on the wrapping element\n */\n readonly?: boolean;\n\n /**\n * Toggles the ability to interact with the rating component.\n * When `true` (the default), the Rating component can be as a form input\n * to gather user feedback.\n *\n * When false, only the `@value` will be shown, and it cannot be changed.\n */\n interactive?: boolean;\n\n /**\n * Callback when the selected rating changes.\n * Can include half-ratings if the iconHalf argument is passed.\n */\n onChange?: (value: number) => void;\n };\n\n Blocks: {\n default: [\n rating: {\n /**\n * The maximum rating\n */\n max: number;\n /**\n * The maxium rating\n */\n total: number;\n /**\n * The current rating\n */\n value: number;\n /**\n * The name shared by the field group\n */\n name: string;\n /**\n * If the rating can be changed\n */\n isReadonly: boolean;\n /**\n * If the rating can be changed\n */\n isChangeable: boolean;\n /**\n * The stars / items radio group\n */\n Stars: WithBoundArgs<\n typeof Stars,\n \"stars\" | \"icon\" | \"isReadonly\" | \"name\" | \"total\" | \"currentValue\"\n >;\n /**\n * Input range for adjusting the rating via fractional means\n */\n Range: WithBoundArgs<\n typeof RatingRange,\n \"step\" | \"max\" | \"value\" | \"name\" | \"handleChange\"\n >;\n },\n ];\n label: [\n state: {\n /**\n * The current rating\n */\n value: number;\n\n /**\n * The maximum rating\n */\n total: number;\n },\n ];\n };\n}\n\nexport class Rating extends Component<Signature> {\n name = `rating-${uniqueId()}`;\n\n get icon() {\n return this.args.icon ?? \"★\";\n }\n\n get isInteractive() {\n return this.args.interactive ?? true;\n }\n\n get isChangeable() {\n const readonly = this.args.readonly ?? false;\n\n return !readonly && this.isInteractive;\n }\n\n get isReadonly() {\n return !this.isChangeable;\n }\n\n get needsDescription() {\n return !this.isInteractive;\n }\n\n <template>\n <RatingState\n @max={{@max}}\n @step={{@step}}\n @value={{@value}}\n @name={{this.name}}\n @readonly={{this.isReadonly}}\n @onChange={{@onChange}}\n as |r publicState|\n >\n <fieldset\n class=\"ember-primitives__rating\"\n role=\"radiogroup\"\n aria-readonly={{if this.isReadonly \"true\"}}\n data-total={{r.total}}\n data-value={{r.value}}\n data-readonly={{this.isReadonly}}\n {{! We use event delegation, this isn't a primary interactive -- we're capturing events from inputs }}\n {{! template-lint-disable no-invalid-interactive }}\n {{on \"click\" r.handleClick}}\n ...attributes\n >\n {{#let\n (component\n Stars\n stars=r.stars\n icon=this.icon\n isReadonly=this.isReadonly\n name=this.name\n total=r.total\n currentValue=r.value\n )\n as |RatingStars|\n }}\n\n {{#if (has-block)}}\n {{yield\n (hash\n max=r.total\n total=r.total\n value=r.value\n name=this.name\n isReadonly=this.isReadonly\n isChangeable=this.isChangeable\n Stars=RatingStars\n Range=(component\n RatingRange\n step=r.step\n max=r.max\n value=r.value\n name=this.name\n handleChange=r.handleChange\n )\n )\n }}\n {{else}}\n {{#if this.needsDescription}}\n {{#if (has-block \"label\")}}\n {{yield publicState to=\"label\"}}\n {{else}}\n <span visually-hidden class=\"ember-primitives__rating__label\">Rated\n {{r.value}}\n out of\n {{r.total}}</span>\n {{/if}}\n {{else}}\n {{#if (has-block \"label\")}}\n <legend>\n {{yield publicState to=\"label\"}}\n </legend>\n {{/if}}\n {{/if}}\n\n <RatingStars />\n {{/if}}\n {{/let}}\n\n </fieldset>\n </RatingState>\n </template>\n}\n"],"names":["RatingRange","setComponentTemplate","precompileTemplate","strictMode","scope","on","templateOnly","isString","x","lte","a","b","Stars","uniqueId","Object","RatingState","Component","localCopy","i","value","_value","step","args","max","stars","result","current","push","n","prototype","cached","setRating","readonly","onChange","setFromString","assert","num","parseFloat","isNaN","handleClick","event","isValid","target","HTMLInputElement","name","type","preventDefault","selected","handleChange","hash","Rating","icon","isInteractive","interactive","isChangeable","isReadonly","needsDescription"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-primitives",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "description": "Making apps easier to build",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -7,7 +7,8 @@ import type { AccordionContentExternalSignature } from "./public.ts";
7
7
  interface Signature extends AccordionContentExternalSignature {
8
8
  Args: {
9
9
  isExpanded: boolean;
10
- value: string;
10
+ triggerId: string;
11
+ contentId: string;
11
12
  disabled?: boolean;
12
13
  };
13
14
  }
@@ -16,7 +17,8 @@ export class AccordionContent extends Component<Signature> {
16
17
  <template>
17
18
  <div
18
19
  role="region"
19
- id={{@value}}
20
+ id={{@contentId}}
21
+ aria-labelledby={{@triggerId}}
20
22
  data-state={{getDataState @isExpanded}}
21
23
  hidden={{this.isHidden}}
22
24
  data-disabled={{@disabled}}
@@ -8,7 +8,8 @@ import type { TOC } from "@ember/component/template-only";
8
8
 
9
9
  interface Signature extends AccordionHeaderExternalSignature {
10
10
  Args: {
11
- value: string;
11
+ triggerId: string;
12
+ contentId: string;
12
13
  isExpanded: boolean;
13
14
  disabled?: boolean;
14
15
  toggleItem: () => void;
@@ -26,7 +27,12 @@ export const AccordionHeader: TOC<Signature> = <template>
26
27
  {{yield
27
28
  (hash
28
29
  Trigger=(component
29
- Trigger value=@value isExpanded=@isExpanded disabled=@disabled toggleItem=@toggleItem
30
+ Trigger
31
+ triggerId=@triggerId
32
+ contentId=@contentId
33
+ isExpanded=@isExpanded
34
+ disabled=@disabled
35
+ toggleItem=@toggleItem
30
36
  )
31
37
  )
32
38
  }}
@@ -1,6 +1,7 @@
1
1
  import Component from "@glimmer/component";
2
2
  import { hash } from "@ember/helper";
3
3
 
4
+ import { uniqueId } from "../../utils.ts";
4
5
  import Content from "./content.gts";
5
6
  import Header from "./header.gts";
6
7
 
@@ -19,6 +20,14 @@ interface Signature extends AccordionItemExternalSignature {
19
20
  }
20
21
 
21
22
  export class AccordionItem extends Component<Signature> {
23
+ /**
24
+ * `@value` only has to be unique within one accordion.
25
+ * Element ids have to be unique across the document.
26
+ */
27
+ id = `ember-primitives__accordion-${uniqueId()}`;
28
+ triggerId = `${this.id}__trigger`;
29
+ contentId = `${this.id}__content`;
30
+
22
31
  <template>
23
32
  <div data-state={{getDataState this.isExpanded}} data-disabled={{@disabled}} ...attributes>
24
33
  {{yield
@@ -26,12 +35,19 @@ export class AccordionItem extends Component<Signature> {
26
35
  isExpanded=this.isExpanded
27
36
  Header=(component
28
37
  Header
29
- value=@value
38
+ triggerId=this.triggerId
39
+ contentId=this.contentId
30
40
  isExpanded=this.isExpanded
31
41
  disabled=@disabled
32
42
  toggleItem=this.toggleItem
33
43
  )
34
- Content=(component Content value=@value isExpanded=this.isExpanded disabled=@disabled)
44
+ Content=(component
45
+ Content
46
+ triggerId=this.triggerId
47
+ contentId=this.contentId
48
+ isExpanded=this.isExpanded
49
+ disabled=@disabled
50
+ )
35
51
  )
36
52
  }}
37
53
  </div>
@@ -29,7 +29,10 @@ export interface AccordionHeaderExternalSignature {
29
29
  /**
30
30
  * The AccordionTrigger component.
31
31
  */
32
- Trigger: WithBoundArgs<typeof Trigger, 'value' | 'isExpanded' | 'disabled' | 'toggleItem'>;
32
+ Trigger: WithBoundArgs<
33
+ typeof Trigger,
34
+ 'triggerId' | 'contentId' | 'isExpanded' | 'disabled' | 'toggleItem'
35
+ >;
33
36
  },
34
37
  ];
35
38
  };
@@ -47,11 +50,17 @@ export interface AccordionItemExternalSignature {
47
50
  /**
48
51
  * The AccordionHeader component.
49
52
  */
50
- Header: WithBoundArgs<typeof Header, 'value' | 'isExpanded' | 'disabled' | 'toggleItem'>;
53
+ Header: WithBoundArgs<
54
+ typeof Header,
55
+ 'triggerId' | 'contentId' | 'isExpanded' | 'disabled' | 'toggleItem'
56
+ >;
51
57
  /**
52
58
  * The AccordionContent component.
53
59
  */
54
- Content: WithBoundArgs<typeof Content, 'value' | 'isExpanded' | 'disabled'>;
60
+ Content: WithBoundArgs<
61
+ typeof Content,
62
+ 'triggerId' | 'contentId' | 'isExpanded' | 'disabled'
63
+ >;
55
64
  },
56
65
  ];
57
66
  };
@@ -8,7 +8,8 @@ import type { TOC } from "@ember/component/template-only";
8
8
  interface Signature extends AccordionTriggerExternalSignature {
9
9
  Args: {
10
10
  isExpanded: boolean;
11
- value: string;
11
+ triggerId: string;
12
+ contentId: string;
12
13
  disabled?: boolean;
13
14
  toggleItem: () => void;
14
15
  };
@@ -17,8 +18,9 @@ interface Signature extends AccordionTriggerExternalSignature {
17
18
  export const AccordionTrigger: TOC<Signature> = <template>
18
19
  <button
19
20
  type="button"
20
- aria-controls={{@value}}
21
- aria-expanded={{@isExpanded}}
21
+ id={{@triggerId}}
22
+ aria-controls={{@contentId}}
23
+ aria-expanded={{if @isExpanded "true" "false"}}
22
24
  data-state={{getDataState @isExpanded}}
23
25
  data-disabled={{@disabled}}
24
26
  aria-disabled={{if @disabled "true" "false"}}
@@ -29,6 +29,7 @@ import { getTabster } from "tabster";
29
29
  import { localCopy } from "tracked-toolbox";
30
30
 
31
31
  import { Link, type Signature as LinkSignature } from "./link.gts";
32
+ import { VisuallyHidden } from "./visually-hidden.gts";
32
33
 
33
34
  import type { TOC } from "@ember/component/template-only";
34
35
  import type Owner from "@ember/owner";
@@ -297,6 +298,12 @@ export interface ItemsSignature {
297
298
  * Defaults to "Search".
298
299
  */
299
300
  placeholder?: string;
301
+ /**
302
+ * What a screen reader announces when the number of results changes.
303
+ *
304
+ * Defaults to "1 result" and "3 results".
305
+ */
306
+ resultsText?: (count: number) => string;
300
307
  };
301
308
  /**
302
309
  * No blocks: this form renders the rows. Passing one is an error.
@@ -348,6 +355,10 @@ export interface BlockSignature {
348
355
  * Not for this form: set it on `Input` yourself.
349
356
  */
350
357
  placeholder?: never;
358
+ /**
359
+ * Not for this form: the rows are yours, and so is announcing them.
360
+ */
361
+ resultsText?: never;
351
362
  /**
352
363
  * Called every time a row is chosen. Hand it `close` to make a modal
353
364
  * palette close itself.
@@ -370,6 +381,8 @@ export interface BlockSignature {
370
381
  setQuery: (query: string) => void;
371
382
  /**
372
383
  * The `<input>`, wired as a combobox over `List`.
384
+ *
385
+ * It needs a name: pass `aria-label`, or an `id` and a `<label>`.
373
386
  */
374
387
  Input: WithBoundArgs<
375
388
  typeof Input,
@@ -432,6 +445,13 @@ export class CommandPalette extends Component<Signature> {
432
445
  return this.args.placeholder ?? "Search";
433
446
  }
434
447
 
448
+ get resultsText() {
449
+ const count = this.args.items?.length ?? 0;
450
+ const custom = this.args.resultsText?.(count);
451
+
452
+ return custom ?? (count === 1 ? "1 result" : `${count} results`);
453
+ }
454
+
435
455
  /**
436
456
  * Whether `@items` was passed, not whether it has anything in it: an empty
437
457
  * array is falsy in a template, and a palette whose results have gone away
@@ -709,6 +729,8 @@ export class CommandPalette extends Component<Signature> {
709
729
  </l.Item>
710
730
  {{/each}}
711
731
  </PaletteList>
732
+
733
+ <VisuallyHidden role="status">{{this.resultsText}}</VisuallyHidden>
712
734
  {{else}}
713
735
  {{yield (hash query=this.query setQuery=this.setQuery Input=PaletteInput List=PaletteList)}}
714
736
  {{/if}}
@@ -14,6 +14,11 @@ import { localCopy } from "tracked-toolbox";
14
14
  import type { TOC } from "@ember/component/template-only";
15
15
  import type { ModifierLike, WithBoundArgs } from "@glint/template";
16
16
 
17
+ interface RegisterSignature {
18
+ Element: HTMLDialogElement;
19
+ Args: { Named: { open: boolean | undefined } };
20
+ }
21
+
17
22
  const DialogElement: TOC<{
18
23
  Element: HTMLDialogElement;
19
24
  Args: {
@@ -29,11 +34,11 @@ const DialogElement: TOC<{
29
34
  /**
30
35
  * @internal
31
36
  */
32
- register: ModifierLike<{ Element: HTMLDialogElement }>;
37
+ register: ModifierLike<RegisterSignature>;
33
38
  };
34
39
  Blocks: { default: [] };
35
40
  }> = <template>
36
- <dialog ...attributes open={{@open}} {{on "close" @onClose}} {{@register}}>
41
+ <dialog ...attributes {{on "close" @onClose}} {{@register open=@open}}>
37
42
  {{yield}}
38
43
  </dialog>
39
44
  </template>;
@@ -144,7 +149,14 @@ class ModalDialog extends Component<Signature> {
144
149
 
145
150
  @tracked declare dialogElement: HTMLDialogElement | undefined;
146
151
 
147
- register = eModifier((element: HTMLDialogElement) => {
152
+ register = eModifier<RegisterSignature>((element, _, { open }) => {
153
+ /** The `open` attribute shows a non-modal dialog, only `showModal` is modal. */
154
+ if (open && !element.open) {
155
+ element.showModal();
156
+ } else if (open === false && element.open) {
157
+ element.close();
158
+ }
159
+
148
160
  /**
149
161
  * This is very sad.
150
162
  *
@@ -14,6 +14,11 @@ import { localCopy } from "tracked-toolbox";
14
14
  import type { TOC } from "@ember/component/template-only";
15
15
  import type { ModifierLike, WithBoundArgs } from "@glint/template";
16
16
 
17
+ interface RegisterSignature {
18
+ Element: HTMLDialogElement;
19
+ Args: { Named: { open: boolean | undefined } };
20
+ }
21
+
17
22
  const DrawerElement: TOC<{
18
23
  Element: HTMLDialogElement;
19
24
  Args: {
@@ -29,11 +34,11 @@ const DrawerElement: TOC<{
29
34
  /**
30
35
  * @internal
31
36
  */
32
- register: ModifierLike<{ Element: HTMLDialogElement }>;
37
+ register: ModifierLike<RegisterSignature>;
33
38
  };
34
39
  Blocks: { default: [] };
35
40
  }> = <template>
36
- <dialog ...attributes open={{@open}} {{on "close" @onClose}} {{@register}}>
41
+ <dialog ...attributes {{on "close" @onClose}} {{@register open=@open}}>
37
42
  {{yield}}
38
43
  </dialog>
39
44
  </template>;
@@ -144,7 +149,14 @@ class DrawerDialog extends Component<Signature> {
144
149
 
145
150
  @tracked declare drawerElement: HTMLDialogElement | undefined;
146
151
 
147
- register = eModifier((element: HTMLDialogElement) => {
152
+ register = eModifier<RegisterSignature>((element, _, { open }) => {
153
+ /** The `open` attribute shows a non-modal drawer, only `showModal` is modal. */
154
+ if (open && !element.open) {
155
+ element.showModal();
156
+ } else if (open === false && element.open) {
157
+ element.close();
158
+ }
159
+
148
160
  /**
149
161
  * This is very sad.
150
162
  *
@@ -159,6 +159,7 @@ export const Link: TOC<Signature> = <template>
159
159
  {{else}}
160
160
  <a
161
161
  data-active={{l.isActive}}
162
+ aria-current={{if l.isActive (if @activeOnSubPaths "true" "page")}}
162
163
  href={{if @href @href "##missing##"}}
163
164
  {{on "click" l.handleClick}}
164
165
  ...attributes
@@ -183,6 +183,8 @@ const installContent = eModifier<{
183
183
  // listen for the escape key
184
184
  function onDocumentKeydown(e: KeyboardEvent) {
185
185
  if (isOpen.current && e.key === "Escape") {
186
+ /** Otherwise the same key press also closes a `<dialog>` around the menu. */
187
+ e.preventDefault();
186
188
  isOpen.current = false;
187
189
  }
188
190
  }
@@ -226,6 +228,7 @@ const Content: TOC<PrivateContentSignature> = <template>
226
228
  <@PopoverContent
227
229
  id={{@contentId}}
228
230
  role="menu"
231
+ aria-labelledby={{@triggerElement.current.id}}
229
232
  data-tabster={{TABSTER_CONFIG_CONTENT}}
230
233
  tabindex="0"
231
234
  {{installContent isOpen=@isOpen triggerElement=@triggerElement}}
@@ -292,6 +295,10 @@ const trigger = eModifier<PrivateTriggerModifierSignature>(
292
295
 
293
296
  element.addEventListener("click", onTriggerClick);
294
297
 
298
+ if (!element.id) {
299
+ element.id = `${contentId}__trigger`;
300
+ }
301
+
295
302
  triggerElement.current = element;
296
303
 
297
304
  setReference(element);
@@ -45,7 +45,7 @@ const Fields: TOC<{
45
45
  name="code{{i}}"
46
46
  type="text"
47
47
  inputmode="numeric"
48
- autocomplete="off"
48
+ autocomplete="one-time-code"
49
49
  ...attributes
50
50
  {{on "click" selectAll}}
51
51
  {{on "paste" handlePaste}}
@@ -93,7 +93,7 @@ export const OTP: TOC<{
93
93
 
94
94
  /**
95
95
  * Whether or not to auto-submit after the code has been pasted
96
- * in to the collective "field". Default is true
96
+ * in to the collective "field". Default is false
97
97
  */
98
98
  autoSubmit?: boolean;
99
99
  };
@@ -44,13 +44,21 @@ export function handlePaste(event: Event) {
44
44
  // multiple text fields
45
45
  event.preventDefault();
46
46
 
47
- const value = clipboardData.getData('Text');
48
- const digits = value;
47
+ fillFrom(target, clipboardData.getData('Text'));
48
+
49
+ // Selected again, so that another paste gives the same result.
50
+ target.select();
51
+
52
+ // A cancelled paste fires no input event, and @onChange listens for that event.
53
+ target.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertFromPaste' }));
54
+ }
55
+
56
+ function fillFrom(target: HTMLInputElement, code: string) {
49
57
  let i = 0;
50
58
  let currElement: HTMLInputElement | null = target;
51
59
 
52
60
  while (currElement) {
53
- currElement.value = digits[i++] || '';
61
+ currElement.value = code[i++] || '';
54
62
 
55
63
  const next = nextInput(currElement);
56
64
 
@@ -60,11 +68,6 @@ export function handlePaste(event: Event) {
60
68
  break;
61
69
  }
62
70
  }
63
-
64
- // We want to select the first field again
65
- // so that if someone holds paste, or
66
- // pastes again, they get the same result.
67
- target.select();
68
71
  }
69
72
 
70
73
  export function handleNavigation(event: KeyboardEvent) {
@@ -143,17 +146,27 @@ export const autoAdvance = (event: Event) => {
143
146
  event.target instanceof HTMLInputElement
144
147
  );
145
148
 
149
+ if ('inputType' in event && event.inputType === 'insertFromPaste') return;
150
+
146
151
  const value = event.target.value;
147
152
 
148
153
  if (value.length === 0) return;
149
154
 
150
- if (value.length > 0) {
151
- if ('data' in event && event.data && typeof event.data === 'string') {
152
- event.target.value = event.data;
153
- }
155
+ const data = 'data' in event && typeof event.data === 'string' ? event.data : '';
156
+
157
+ // Browser autofill puts the whole code in to the first field.
158
+ if (data.length > 1 || (!data && value.length > 1)) {
159
+ fillFrom(event.target, data || value);
160
+ event.target.select();
154
161
 
155
- return focusRight(event);
162
+ return;
156
163
  }
164
+
165
+ if (data) {
166
+ event.target.value = data;
167
+ }
168
+
169
+ return focusRight(event);
157
170
  };
158
171
 
159
172
  export function getCollectiveValue(elementTarget: EventTarget | null, length: number) {
@@ -6,12 +6,15 @@ export const RatingRange: TOC<{
6
6
  Element: HTMLInputElement;
7
7
  Args: {
8
8
  name: string;
9
+ step: number;
9
10
  max: number;
10
11
  value: number;
11
12
  handleChange: (event: Event) => void;
12
13
  };
13
14
  }> = <template>
14
15
  <input
16
+ min="0"
17
+ step={{@step}}
15
18
  ...attributes
16
19
  name={{@name}}
17
20
  type="range"
@@ -103,7 +103,10 @@ export interface Signature {
103
103
  /**
104
104
  * Input range for adjusting the rating via fractional means
105
105
  */
106
- Range: WithBoundArgs<typeof RatingRange, "max" | "value" | "name" | "handleChange">;
106
+ Range: WithBoundArgs<
107
+ typeof RatingRange,
108
+ "step" | "max" | "value" | "name" | "handleChange"
109
+ >;
107
110
  },
108
111
  ];
109
112
  label: [
@@ -159,6 +162,8 @@ export class Rating extends Component<Signature> {
159
162
  >
160
163
  <fieldset
161
164
  class="ember-primitives__rating"
165
+ role="radiogroup"
166
+ aria-readonly={{if this.isReadonly "true"}}
162
167
  data-total={{r.total}}
163
168
  data-value={{r.value}}
164
169
  data-readonly={{this.isReadonly}}
@@ -193,7 +198,7 @@ export class Rating extends Component<Signature> {
193
198
  Range=(component
194
199
  RatingRange
195
200
  step=r.step
196
- max=r.total
201
+ max=r.max
197
202
  value=r.value
198
203
  name=this.name
199
204
  handleChange=r.handleChange
@@ -50,7 +50,6 @@ export const Stars: TOC<{
50
50
  type="radio"
51
51
  name={{@name}}
52
52
  value={{star}}
53
- readonly={{@isReadonly}}
54
53
  checked={{Object.is star @currentValue}}
55
54
  />
56
55
  </span>
@@ -22,6 +22,7 @@ export class RatingState extends Component<{
22
22
  stars: number[];
23
23
  step: number;
24
24
  value: number;
25
+ max: number;
25
26
  total: number;
26
27
  handleClick: (event: Event) => void;
27
28
  handleChange: (event: Event) => void;
@@ -110,6 +111,13 @@ export class RatingState extends Component<{
110
111
 
111
112
  if (!isValid) return;
112
113
 
114
+ if (this.args.readonly) {
115
+ /** `readonly` has no effect on a radio, cancelling is what restores the checked one. */
116
+ event.preventDefault();
117
+
118
+ return;
119
+ }
120
+
113
121
  const selected = event.target?.value;
114
122
 
115
123
  this.setFromString(selected);
@@ -131,6 +139,7 @@ export class RatingState extends Component<{
131
139
  {{yield
132
140
  (hash
133
141
  stars=this.stars
142
+ max=this.max
134
143
  total=this.stars.length
135
144
  handleClick=this.handleClick
136
145
  handleChange=this.handleChange
@@ -160,6 +160,13 @@ class ThumbComponent extends Component<{
160
160
  return this.args.thumb?.inputValue ?? this.args.value ?? this.args.store.internalMin;
161
161
  }
162
162
 
163
+ /** With tick values the input holds an index, which means nothing when read aloud. */
164
+ get valueText(): string | undefined {
165
+ const tick = this.args.store.tickValues?.[this.value];
166
+
167
+ return tick === undefined ? undefined : String(tick);
168
+ }
169
+
163
170
  get isActive(): boolean {
164
171
  return this.args.store.activeThumbIndex === this.index;
165
172
  }
@@ -205,6 +212,7 @@ class ThumbComponent extends Component<{
205
212
 
206
213
  <template>
207
214
  <input
215
+ aria-valuetext={{this.valueText}}
208
216
  ...attributes
209
217
  class="ember-primitives__slider__thumb-input"
210
218
  type="range"